【Xilem基础语法学与练】第8课:条件渲染(one_of)

一、为什么需要条件渲染?

前几课我们写的界面都是"固定"的------不管状态怎么变,界面上有哪些组件是写死的,只是文本内容会变。但实际应用中,经常需要根据状态动态决定显示哪些组件、隐藏哪些组件。

举几个常见场景:

  • 用户未登录时显示"登录"按钮,已登录时显示用户名和"退出"按钮
  • 加载数据时显示"加载中...",加载完成后显示数据列表,加载失败时显示错误提示
  • 购物车为空时显示"暂无商品",有商品时显示商品列表

这些场景的共同点是:不同状态下,界面的"结构"不同------不是文本内容变了,而是组件本身出现了或消失了。这就是条件渲染要解决的问题。

二、Xilem 的条件渲染方案:one_of

Xilem 提供了一个专门的 View 叫 one_of,它的作用是:根据条件从多个分支中选一个来渲染。

基本语法:

one_of(condition, (branch_a, branch_b))

  • 第一个参数:条件表达式,类型为 bool
  • 第二个参数:一个元组,包含两个分支
    • 条件为 false 时,渲染第一个分支
    • 条件为 true 时,渲染第二个分支

三、最简单的例子:显示/隐藏

rust 复制代码
use xilem::view::{one_of, flex_col, label, text_button};

struct AppState {
    show_detail: bool,
}

impl Default for AppState {
    fn default() -> Self {
        AppState { show_detail: false }
    }
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        text_button("切换显示", |data: &mut AppState| {
            data.show_detail = !data.show_detail;
        }),
        // 条件渲染:show_detail 为 false 显示"已隐藏",为 true 显示"详细内容"
        one_of(data.show_detail, (
            label("已隐藏"),
            label("这是详细内容,现在你看到我了!"),
        )),
    ))
}

运行效果:

点击按钮,下方文字在"已隐藏"和"这是详细内容,现在你看到我了!"之间切换。

关键点:

one_of 不是简单地隐藏/显示------它在 false 分支和 true 分支之间完全切换,不渲染的那个分支不会出现在视图树中。

四、实际场景:登录状态切换

一个更贴近实际的例子------根据登录状态显示不同的界面:

rust 复制代码
use xilem::view::{one_of, flex_col, row, label, text_button};

struct AppState {
    logged_in: bool,
    username: String,
}

impl Default for AppState {
    fn default() -> Self {
        AppState {
            logged_in: false,
            username: String::new(),
        }
    }
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        label("欢迎使用本系统"),

        // 根据登录状态显示不同的 UI
        one_of(data.logged_in, (
            // false 分支:未登录
            flex_col((
                label("你尚未登录"),
                text_button("登录", |data: &mut AppState| {
                    data.logged_in = true;
                    data.username = "张三".to_string();
                }),
            )),
            // true 分支:已登录
            flex_col((
                row((
                    label("当前用户:"),
                    label(data.username.clone()),
                )),
                text_button("退出登录", |data: &mut AppState| {
                    data.logged_in = false;
                    data.username = String::new();
                }),
            )),
        )),
    ))
}

这个例子展示了条件渲染的核心价值:

不同状态下,界面的组件结构完全不同。未登录时只有"登录"按钮,已登录时显示用户名和"退出"按钮。

五、嵌套条件:多层判断

条件渲染可以嵌套使用,实现更复杂的逻辑:

rust 复制代码
struct AppState {
    status: u8, // 0=加载中, 1=成功, 2=失败
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        // 第一层:判断是否加载成功
        one_of(data.status == 1, (
            // 不是成功状态
            one_of(data.status == 2, (
                // 不是失败,那就是加载中
                label("⏳ 正在加载数据..."),
                // 是失败状态
                flex_col((
                    label("❌ 加载失败,请重试"),
                    text_button("重新加载", |data: &mut AppState| {
                        data.status = 0;
                    }),
                )),
            )),
            // 是成功状态
            label("✅ 数据加载完成,这是内容"),
        )),
    ))
}

嵌套条件的阅读方法:从外到内,逐层判断。

  • 外层 one_of(data.status == 1, ...):先判断是否成功
    • 如果不是成功 → 进入第一个分支,再判断是否失败
      • 如果不是失败 → 显示"加载中"
      • 如果是失败 → 显示错误提示和重试按钮
    • 如果是成功 → 显示数据内容

六、条件渲染和三元表达式的区别

很多有 Web 开发经验的同学会问:能不能直接用 Rust 的 if-else 表达式来代替 one_of?

rust 复制代码
// 这样写行不行?
if data.logged_in {
    label("已登录")
} else {
    label("未登录")
}

不行。原因是 Rust 的类型rust统。one_of 的两个分支返回的是同一个 one_of View 类型,框架可以统一处理。而 if-else 的两个分支返回的是不同的 View 类型(两个不同的 Label 实例),Rust 编译器不允许函数在不同分支返回不同类型。

one_of 本质上是把"选择哪个分支"这个决策延迟到运行时,由框架来处理类型擦除和 diff 更新,绕过了 Rust 编译器的类型限制。

七、条件渲染的 diff 行为

理解 one_of 的 diff 行为很重要,这直接影响性能和用户体验:

场景1:条件不变,只是分支内部状态变化

rust 复制代码
one_of(data.show_detail, (
    label(format!("隐藏状态,计数:{}", data.count)),
    label(format!("显示状态,计数:{}", data.count)),
))

当 count 变化但 show_detail 不变时,框架只更新当前激活分支内的文本,不会切换分支。

场景2:条件变化,分支切换

当 show_detail 从 false 变为 true 时,框架会:

  1. 销毁第一个分支("隐藏状态")的所有 Widget
  2. 创建第二个分支("显示状态")的所有 Widget
  3. 重新布局并渲染

这意味着分支切换是有成本的------不是简单的显示/隐藏,而是真正的销毁和重建。所以:

  • 如果两个分支结构差异很大(比如登录前 vs 登录后),用 one_of 是正确的选择
  • 如果只是简单的文本变化,直接更新文本内容比切换分支更高效

八、常见错误与排查

错误1:条件变量没有触发重绘

rust 复制代码
// 错误:在闭包内部定义条件变量
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    let show = false; // 每次闭包执行都重置为 false
    one_of(show, (
        label("永远显示这个"),
        label("永远不会显示这个"),
    ))
}

和第五课的状态管理陷阱一样------条件变量必须存储在 AppState 中,不能定义在闭包内部。

错误2:两个分支的类型不一致

one_of 要求两个分支都是合法的 WidgetView,如果某个分支返回了不兼容的类型,编译器会报错。确保两个分支都返回完整的 View。

九、练习题

练习1:基础条件渲染

实现一个开关效果:一个按钮文本为"切换",点击后在下方显示"开"或"关"。初始状态为"关"。窗口 400×200。

练习2:多状态界面

实现一个三态加载器,状态分别为:Loading(显示"加载中...")、Success(显示"加载成功!"加一个"重置"按钮)、Error(显示"加载失败"加一个"重试"按钮)。点击"重置"回到 Loading,点击"重试"也回到 Loading。窗口 400×250。

提示:用嵌套 one_of 或者用一个枚举 + 多次判断来实现。

练习3:条件布局组合

实现一个界面:顶部有一个按钮"切换布局",点击后在两种布局之间切换:

  • 布局A:垂直排列三个文本"第一行"、"第二行"、"第三行"
  • 布局B:水平排列三个文本"左"、"中"、"右"

窗口 500×300。

练习4:思考题

下面这段代码中,点击按钮后界面会怎样变化?为什么?

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    one_of(data.count > 5, (
        label(format!("count = {},小于等于5", data.count)),
        label(format!("count = {},大于5", data.count)),
    ))
}

假设初始 count = 0,每次点击按钮 count += 1。当 count 从 5 变成 6 时,界面会发生什么?当 count 从 6 变成 7 时,界面又会发生什么?两者的行为有什么区别?

十、练习题答案贴代码

练习1答案:

rust 复制代码
use xilem::view::{one_of, flex_col, label, text_button};

struct AppState {
    is_on: bool,
}

impl Default for AppState {
    fn default() -> Self {
        AppState { is_on: false }
    }
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        text_button("切换", |data: &mut AppState| {
            data.is_on = !data.is_on;
        }),
        one_of(data.is_on, (
            label("关"),
            label("开"),
        )),
    ))
}

练习2答案:

rust 复制代码
use xilem::view::{one_of, flex_col, label, text_button};

#[derive(Clone, Copy, PartialEq)]
enum LoadStatus {
    Loading,
    Success,
    Error,
}

struct AppState {
    status: LoadStatus,
}

impl Default for AppState {
    fn default() -> Self {
        AppState { status: LoadStatus::Loading }
    }
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        // 第一层:是否成功?
        one_of(data.status == LoadStatus::Success, (
            // 不是成功
            one_of(data.status == LoadStatus::Error, (
                // 不是错误,那就是加载中
                label("加载中..."),
                // 是错误
                flex_col((
                    label("加载失败"),
                    text_button("重试", |data: &mut AppState| {
                        data.status = LoadStatus::Loading;
                    }),
                )),
            )),
            // 是成功
            flex_col((
                label("加载成功!"),
                text_button("重置", |data: &mut AppState| {
                    data.status = LoadStatus::Loading;
                }),
            )),
        )),
    ))
}

练习3答案:

rust 复制代码
use xilem::view::{one_of, flex_col, row, label, text_button, Axis, flex};

struct AppState {
    use_layout_a: bool,
}

impl Default for AppState {
    fn default() -> Self {
        AppState { use_layout_a: true }
    }
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex_col((
        text_button("切换布局", |data: &mut AppState| {
            data.use_layout_a = !data.use_layout_a;
        }),
        one_of(data.use_layout_a, (
            // 布局B:水平排列
            row((
                label("左"),
                label("中"),
                label("右"),
            )),
            // 布局A:垂直排列
            flex_col((
                label("第一行"),
                label("第二行"),
                label("第三行"),
            )),
        )),
    ))
}

注意:one_of 的元组中,第一个元素对应 false,第二个元素对应 true。所以 use_layout_a 为 true 时显示第二个分支(布局A),为 false 时显示第一个分支(布局B)。

练习4答案:

当 count 从 5 变成 6 时:条件 count > 5 从 false 变为 true,one_of 会切换分支------销毁第一个分支的 Widget,创建第二个分支的 Widget。用户看到文字从"count = 5,小于等于5"变成"count = 6,大于5",但底层发生了完整的分支切换。

当 count 从 6 变成 7 时:条件 count > 5 仍然是 true,one_of 不会切换分支,只是在当前分支(第二个)内部更新文本内容。用户看到文字从"count = 6,大于5"变成"count = 7,大于5",底层只是文本内容更新,没有分支切换。

两者的本质区别:前者发生了分支切换(销毁旧 Widget + 创建新 Widget),后者只是同一分支内的文本更新(只更新文本内容)。分支切换的开销更大,所以在设计条件渲染时,应尽量让条件在"稳定状态"下保持不变,避免不必要的分支切换。

十一、本课知识点总结

  • one_of(condition, (false_branch, true_branch)) 是 Xilem 的条件渲染方案,根据布尔条件在两个分支之间切换。
  • 条件为 false 渲染第一个分支,条件为 true 渲染第二个分支。
  • 分支切换是销毁+重建,不是简单的显示/隐藏,有性能开销。
  • 嵌套 one_of 可以实现多分支判断(类似 if-else if-else)。
  • 不能用 Rust 的 if-else 表达式代替 one_of,因为两个分支返回不同类型,编译器不允许。
  • 条件变量必须存储在 AppState 中,不能定义在视图闭包内部。

下一课可以讲动态列表(for_each),用来渲染数量不确定的元素集合,比如商品列表、聊天记录等。要继续吗?

相关推荐
小雪崩1 小时前
嵌入式学习 day44:51单片机入门
嵌入式硬件·学习·51单片机
浔溺1 小时前
al+大数据每日学习笔记35
大数据·笔记·学习
kyrie_sakura2 小时前
MySQL学习笔记4 -- select的7大子句,子查询
笔记·学习·mysql
UIU1142 小时前
补码运算与整数溢出(上
学习·c#·补码·补码运算
光影少年2 小时前
React18 对RN 的影响
android·前端·react.js·ios·前端框架
qwsaedca3 小时前
在Mac上跑 Kokoro TTS经验总结
rust·mac·tts
qwsaedca3 小时前
Kokoro TTS v1.1 voices 文件格式逆向分析
rust
词却3 小时前
OpenCV学习:人脸识别
人工智能·opencv·学习
xqqxqxxq3 小时前
AI Agent学习:第四章小结及八道思考题(李博杰《深入理解 AI Agent》第四章观后总结)
大数据·人工智能·学习