一、为什么需要条件渲染?
前几课我们写的界面都是"固定"的------不管状态怎么变,界面上有哪些组件是写死的,只是文本内容会变。但实际应用中,经常需要根据状态动态决定显示哪些组件、隐藏哪些组件。
举几个常见场景:
- 用户未登录时显示"登录"按钮,已登录时显示用户名和"退出"按钮
- 加载数据时显示"加载中...",加载完成后显示数据列表,加载失败时显示错误提示
- 购物车为空时显示"暂无商品",有商品时显示商品列表
这些场景的共同点是:不同状态下,界面的"结构"不同------不是文本内容变了,而是组件本身出现了或消失了。这就是条件渲染要解决的问题。
二、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 时,框架会:
- 销毁第一个分支("隐藏状态")的所有 Widget
- 创建第二个分支("显示状态")的所有 Widget
- 重新布局并渲染
这意味着分支切换是有成本的------不是简单的显示/隐藏,而是真正的销毁和重建。所以:
- 如果两个分支结构差异很大(比如登录前 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),用来渲染数量不确定的元素集合,比如商品列表、聊天记录等。要继续吗?