本课目标
- 明确Masonry(砖块水泥)和Xilem(设计图)的定位差异
- 掌握Xilem声明式层不可替代的4个核心价值
- 能判断什么场景该直接用Masonry,什么场景该用Xilem
先明确前提:
Masonry的组件库已经足够完整
首先纠正之前的认知偏差:截至2026年9月,Masonry的底层widget库已经覆盖了所有常用UI组件:除了基础的按钮、标签、文本输入框之外,已经实现了网格布局(Grid)、复选框(Checkbox)、进度条(ProgressBar)、加载指示器(Spinner)、层叠布局(ZStack)、可拖拽分割面板(Split)、分割线(Divider)等所有常见组件,完全具备独立搭建任何复杂UI的能力。
就像你手里有足够多的砖块、水泥、钢筋、玻璃,确实可以盖出任何你想要的房子,哪怕没有设计图,也能靠经验一点点搭出来。
为什么还需要Xilem?——4个Masonry解决不了的核心痛点
痛点1:状态更新要手动操作,规模大了根本维护不动
我们用「计数器」这个最简单的场景对比两种写法:
Masonry命令式写法
// 1. 手动创建所有widgetletcount_label=Label::new("0");letadd_btn=Button::new("加1");// 2. 手动绑定事件,手动更新UIadd_btn.on_click(move|ctx,_,_|{// 每次点击都要手动找到label,手动改文本,手动触发布局count_label.set_text((current_count+1).to_string());ctx.request_layout();current_count+=1;});如果是100项的待办列表,每次新增、删除、勾选完成,你都要手动找到对应的widget,手动修改属性,手动管理widget的增删,代码量会随着功能复杂度指数级增长,改一个地方很容易漏改其他地方,出现UI和数据不一致的bug。
Xilem声明式写法
fnapp_logic(data:&mutCounter)->implWidgetView<Counter>+use<>{flex(Axis::Vertical,(label(format!("{}",data.num)),text_button("加1",|data:&mutCounter|data.num+=1),))}结论:
你只需要描述「UI应该长什么样」,状态变化后框架会自动对比新旧视图的差异,只更新变化的部分,你完全不用关心「哪个widget需要改、怎么改」,代码量只有Masonry的1/3,还不会出状态同步的问题。
痛点2:没有类型安全的状态绑定,容易出运行时bug
Masonry里你可以给一个期望显示字符串的Label传一个i32类型的数据,编译阶段不会报任何错误,只有运行的时候才会出现显示异常甚至panic。
Xilem的lens/map_state状态绑定机制完全基于Rust的类型系统,编译期就会检查状态类型和组件期望的输入类型是否匹配,传错了直接编译不通过,把运行时bug提前到编译阶段解决。
痛点3:状态来源混乱,大型应用容易出同步问题
Masonry没有强制的数据流约束,你可以从按钮的点击回调里改widget属性,也可以从网络请求的回调里直接改widget属性,甚至可以从外部定时任务里改,状态来源不唯一,应用规模大了之后很容易出现「数据已经变了但UI没更新」或者「UI变了但数据没同步」的问题,排查起来非常困难。
Xilem强制单向数据流:所有UI更新只能来自状态变化,所有状态变化只能来自用户事件回调,逻辑完全可预测,出现bug的时候只需要顺着数据流排查,调试成本大幅降低。
痛点4:跨平台复用成本高
Masonry只支持桌面端渲染,如果你想让同一套业务逻辑同时跑在桌面和Web端,需要完全重写一套Web端的UI代码。
Xilem的View层是平台无关的,同一套app_logic业务逻辑代码,桌面端可以对接Masonry的widget树渲染,Web端可以对接xilem_web的DOM树渲染,业务逻辑完全不用修改,跨平台复用成本几乎为零。
什么时候直接用Masonry?
只有两种场景推荐直接用Masonry,不需要Xilem:
- 你在开发自己的UI框架,需要底层的widget控制能力,比如自定义布局算法、渲染管线
- 你的UI是完全静态的,没有任何状态变化,比如纯展示的信息面板、启动页、静态海报
其他所有做应用的场景,都优先用Xilem。
课后练习
- 用Masonry写一个简单的待办列表,支持添加、删除、勾选完成三项功能,记录你写了多少行代码,要处理哪些手动更新的逻辑
- 用Xilem写同样的待办列表,对比两种写法的代码量和维护成本
- 思考:如果你的待办列表有1000项,每次添加一项,Masonry和Xilem分别要处理多少操作?
练习答案贴代码和解读
练习一:Masonry 命令式写法
usemasonry::widget::{Button,Flex,Label,Checkbox,Portal};usemasonry::widgets;structTodoApp{todos:Vec<String>,completed:Vec<bool>,input_text:String,}implTodoApp{fnbuild_ui(&self)->implWidget{letmutroot=Flex::column();// 手动创建输入框letinput=TextInput::new();input.set_text(&self.input_text);// 手动创建添加按钮letadd_btn=Button::new("添加");// 手动创建列表letmutlist=Flex::column();for(i,todo)inself.todos.iter().enumerate(){letmutrow=Flex::row();letcheckbox=Checkbox::new(self.completed[i]);checkbox.on_click(move|ctx,_,_|{// 需要手动找到对应的数据并更新// 问题:这里拿不到 i 对应的数据引用// 必须通过外部状态管理来同步});letlabel=Label::new(todo.as_str());letdelete_btn=Button::new("删除");delete_btn.on_click(move|ctx,_,_|{// 手动删除第 i 项// 问题:删除后所有后续 widget 的索引都变了// 必须手动重建整个列表});row.add_child(checkbox);row.add_child(label);row.add_child(delete_btn);list.add_child(row);}root.add_child(input);root.add_child(add_btn);root.add_child(list);root}// 手动更新方法——每次数据变化都要调用fnrefresh_ui(&self,root:&mutimplWidget){// 1. 清空旧列表// 2. 重新创建所有 widget// 3. 重新绑定所有事件// 4. 手动触发布局和重绘}}代码量估算:约 80-120 行(含事件绑定和手动更新逻辑)
需要手动处理的逻辑清单:
- 添加一项:创建新 widget → 插入到父容器正确位置 → 触发布局
- 删除一项:找到对应 widget → 从父容器移除 → 更新后续所有 widget 的索引 → 触发布局
- 勾选完成:找到对应 checkbox → 更新状态 → 同步数据 → 触发重绘
- 修改文本:找到对应 label → 更新文本 → 触发重绘
- 数据变化后:手动调用 refresh_ui() 重建整个列表
练习二:Xilem 声明式写法
usexilem::view::*;usexilem::widget::Axis;structTodo{text:String,done:bool,}structTodoApp{todos:Vec<Todo>,draft:String,}fnapp_logic(data:&mutTodoApp)->implWidgetView<TodoApp>+use<>{flex(Axis::Vertical,(// 输入框text_input(&data.draft,|data:&mutTodoApp,text:String|{data.draft=text;}),// 添加按钮text_button("添加",|data:&mutTodoApp|{if!data.draft.is_empty(){data.todos.push(Todo{text:data.draft.clone(),done:false,});data.draft.clear();}}),// 动态列表——for_each 自动处理增删 difffor_each(&mutdata.todos,|todo:&mutTodo|todo.text.clone(),|data:&mutTodoApp,idx:usize|{lens(|data:&mutTodoApp|&mutdata.todos[idx],|todo:&mutTodo|{flex(Axis::Horizontal,(label(format!("{} {}",iftodo.done{"✅"}else{"⬜"},todo.text)),text_button(iftodo.done{"取消"}else{"完成"},|todo:&mutTodo|todo.done=!todo.done,),text_button("删除",|data:&mutTodoApp|{data.todos.remove(idx);}),))},)}),))}代码量估算:约 40-50 行
你完全不用手动处理的事:
- 添加一项:for_each 检测到列表变长 → 自动创建新 widget → 插入正确位置
- 删除一项:for_each 检测到列表变短 → 自动移除对应 widget → 其余不动
- 勾选完成:lens 检测到 done 字段变化 → 只重建这一行的 label
- 修改文本:text_input 回调修改 draft → 自动更新输入框显示
- 数据变化后:框架自动 diff → 只更新变化的部分 → 自动触发布局和重绘
练习三:1000 项列表的性能对比
假设列表已有 1000 项,现在在第 500 项的位置插入一项:
Masonry 需要做的操作:
- 创建 1 个新 widget(第 500 项)
- 手动更新第 500~1000 项的索引引用(501 次指针修改)
- 手动从父容器移除旧列表(1000 次 remove_child)
- 手动重新添加所有 widget(1001 次 add_child)
- 手动重新绑定所有事件回调(1001 次 on_click 绑定)
- 手动请求全局布局(1 次 request_layout,触发 1001 个 widget 的布局计算)
- 手动请求全局重绘(1 次 request_paint,触发整个列表区域重绘)
总计:约 4500+ 次手动操作
Xilem 需要做的操作:
- 修改 data.todos(1 次 Vec::insert)
- for_each 的 diff 算法检测到第 500 项是新增(1 次 key 比对)
- 只创建 1 个新 widget(第 500 项)
- 第 0~499 项:key 没变,跳过(0 次操作)
- 第 501~1001 项:key 没变,只更新索引偏移(501 次轻量级指针调整,不重建 widget)
- 自动请求局部布局(只计算第 500 项及后续项的布局)
- 自动请求局部重绘(只重绘第 500 项及被挤下去的区域)
总计:约 1 次数据修改 + 1 次 widget 创建 + 局部布局/重绘
性能差距:Masonry 是 O(n) 的全量重建,Xilem 是 O(1) 的增量更新(插入项数)。列表越大,差距越夸张。
知识点总结
核心概念:声明式 vs 命令式
- 编程思维:命令式(Masonry)是"怎么做"——你告诉框架每一步操作;声明式(Xilem)是"是什么"——你告诉框架最终状态
- 状态管理:命令式需要你手动同步数据和 UI;声明式由框架自动同步,UI 是状态的纯函数
- 更新粒度:命令式由你决定更新什么(容易漏或过度更新);声明式由框架通过 diff 精确计算最小更新集
- 错误模式:命令式的问题运行时才暴露(UI 和数据不一致);声明式的问题编译期就拦截(类型不匹配直接报错)
- 代码量:命令式随复杂度指数增长;声明式随复杂度线性增长
Xilem 声明式层的 4 个不可替代价值:
- 自动增量更新:for_each 基于 key 的 diff 算法,避免全量重建
- 类型安全绑定:lens / map_state 在编译期保证状态类型和组件输入类型匹配
- 单向数据流:所有 UI 变化只能来自状态变化,杜绝状态来源混乱
- 跨平台复用:同一套 app_logic 可对接 Masonry(桌面)或 xilem_web(浏览器)
决策口诀
静态界面用 Masonry,动态应用用 Xilem。
小项目随便选,大项目必须 Xilem。
写框架用 Masonry,写应用用 Xilem。
要不要接着讲第21课?on_click / on_change 与手势系统的实战。