4.5 状态管理
> **本节目标**:完整掌握 RML 的状态管理——`cx.notify()` 的触发机制、Entity 模型、跨视图状态共享、异步状态更新。
4.5.1 状态的本质
在 RML 中,状态就是 ViewModel 的字段。状态管理就是:
1. **修改状态**:在命令方法中修改字段值 2. **通知变化**:调用 `cx.notify()` 告诉 GPUI 状态已变化 3. **触发重绘**:GPUI 重新调用 `Render::render`,UI 读取新值
#[command]
pub fn increment(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1; // 1. 修改状态
cx.notify(); // 2. 通知变化 → 3. 触发重绘
}
4.5.2 `cx.notify()` 详解
通知的本质
`cx.notify()` 告诉 GPUI:"这个 Entity 的状态变了,下次渲染时请重新调用 `render`"。
cx.notify()
↓
GPUI 标记 Entity 为 "dirty"
↓
下一帧时,GPUI 调用 Render::render
↓
RML 生成的代码重新读取所有绑定值
↓
GPUI diff 新旧渲染树,更新实际 UI
何时调用 `cx.notify()`
**规则**:每次修改 ViewModel 字段后,都必须调用 `cx.notify()`。
// ✅ 正确:修改后 notify
#[command]
pub fn update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
cx.notify();
}
// ❌ 错误:忘记 notify,UI 不会更新
#[command]
pub fn bad_update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
// 缺少 cx.notify()
}
批量更新
多次 `cx.notify()` 会在同一帧内合并为一次重绘:
#[command]
pub fn batch_update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.field1 = "new1".into();
cx.notify(); // 不会立即重绘
self.field2 = "new2".into();
cx.notify(); // 不会立即重绘
self.field3 = "new3".into();
cx.notify(); // 函数返回后,下一帧统一重绘
}
💡 **最佳实践**:在命令方法末尾调用一次 `cx.notify()` 即可,无需每次修改字段都调用。
在异步任务中 notify
异步任务中修改状态必须通过 `this.update`:
cx.spawn(|this, mut cx| async move {
let data = fetch_data().await;
let _ = this.update(&mut cx, |this, cx| {
this.data = data;
cx.notify(); // 在 update 闭包内 notify
});
}).detach();
⚠️ **注意**:不能在异步任务中直接捕获 `&mut self`,必须通过 `this.update` 获取 `&mut self`。详见 [第 8 章 · 状态生命周期](../08-lifecycle/state-lifecycle.md)。
4.5.3 Entity 模型
Entity 是什么
ViewModel 通过 `#[derive(IModel)]` 成为 GPUI 管理的 Entity。Entity 是 GPUI 的状态管理单元:
- 每个 Entity 有唯一的 `EntityId` - Entity 由 `App` 统一管理 - 通过 `Entity<T>` 句柄引用
#[derive(IModel)]
#[component]
pub struct MyView {
pub count: i32,
}
// MyView 的实例是一个 Entity
// 通过 Entity<MyView> 句柄引用
Entity 的创建
Entity 在 ViewModel 的 `new()` 方法中创建,由 `RmlApplication` 自动管理:
fn main() {
RmlApplication::new()
.main_window::<MyView>() // 自动创建 MyView 的 Entity
.run()
.unwrap();
}
Entity 的引用
通过 `Entity<T>` 句柄引用其他 ViewModel:
use gpui::Entity;
#[derive(IModel)]
#[component]
pub struct ParentView {
pub child: Entity<ChildView>,
}
4.5.4 跨视图状态共享
方式一:父子引用
父视图持有子视图的 `Entity` 句柄:
#[derive(IModel)]
#[component]
pub struct ParentView {
pub child: Entity<ChildView>,
pub parent_count: i32,
}
impl ParentView {
pub fn new(cx: &mut AppContext) -> Self {
Self {
child: cx.new_model(|_| ChildView::new()),
parent_count: 0,
}
}
#[command]
pub fn increment_both(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.parent_count += 1;
self.child.update(cx, |child, cx| {
child.count += 1;
cx.notify();
});
cx.notify();
}
}
方式二:全局状态
用 GPUI 的 `Global` 机制共享全局状态:
use gpui::Global;
#[derive(Clone)]
pub struct AppTheme {
pub primary_color: Hsla,
pub background: Hsla,
}
impl Global for AppTheme {}
// 在 App 中注册全局状态
fn main() {
RmlApplication::new()
.with_global(AppTheme {
primary_color: rgb(0x1890ff).into(),
background: rgb(0xffffff).into(),
})
.main_window::<MyView>()
.run()
.unwrap();
}
// 在 ViewModel 中访问全局状态
#[derive(IModel)]
#[component]
pub struct MyView {
pub theme: AppTheme,
}
impl MyView {
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
self.theme = cx.global::<AppTheme>().clone();
// 订阅全局状态变化
cx.observe_global::<AppTheme>(|this, cx| {
this.theme = cx.global::<AppTheme>().clone();
cx.notify();
}).detach();
}
}
方式三:上下文(Context)
用 GPUI 的 `Context` 机制在视图树中传递数据:
// 父视图设置上下文
#[command]
pub fn set_user_context(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
cx.set_global(UserContext {
current_user: self.current_user.clone(),
});
}
// 子视图读取上下文
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
if let Some(user_ctx) = cx.try_global::<UserContext>() {
self.current_user = user_ctx.current_user.clone();
}
}
4.5.5 状态更新的模式
模式一:同步更新
最简单的模式,直接修改字段并 notify:
#[command]
pub fn increment(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
cx.notify();
}
模式二:异步更新
用于网络请求、文件 IO 等异步操作:
#[command]
pub fn load_data(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.is_loading = true;
cx.notify();
cx.spawn(|this, mut cx| async move {
let data = fetch_data().await;
let _ = this.update(&mut cx, |this, cx| {
this.data = data;
this.is_loading = false;
cx.notify();
});
}).detach();
}
模式三:批量更新
一次性修改多个相关字段:
#[command]
pub fn refresh(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.is_loading = true;
self.error = None;
cx.notify();
cx.spawn(|this, mut cx| async move {
let result = fetch_all_data().await;
let _ = this.update(&mut cx, |this, cx| {
match result {
Ok((users, posts, comments)) => {
this.users = users;
this.posts = posts;
this.comments = comments;
this.is_loading = false;
this.last_refresh = chrono::Local::now();
}
Err(e) => {
this.error = Some(e.to_string().into());
this.is_loading = false;
}
}
cx.notify();
});
}).detach();
}
模式四:条件更新
根据条件决定是否更新:
#[command]
pub fn update_if_valid(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if self.is_valid() {
self.apply_changes();
cx.notify();
}
}
4.5.6 状态的持久化
保存到本地存储
#[on_unloaded]
pub fn on_unloaded(&mut self, _cx: &mut ViewContext<Self>) {
if let Err(e) = self.save_to_storage() {
log::error!("保存状态失败: {}", e);
}
}
fn save_to_storage(&self) -> Result<(), StorageError> {
let serialized = serde_json::to_string(&self.todos)?;
std::fs::write("todos.json", serialized)?;
Ok(())
}
从本地存储加载
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
if let Err(e) = self.load_from_storage() {
log::warn!("加载状态失败: {}", e);
}
cx.notify();
}
fn load_from_storage(&mut self) -> Result<(), StorageError> {
if let Ok(content) = std::fs::read_to_string("todos.json") {
self.todos = serde_json::from_str(&content)?;
}
Ok(())
}
4.5.7 状态的订阅
订阅其他 Entity
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
// 订阅子视图的变化
cx.observe(&self.child, |this, _child, cx| {
// 子视图变化时更新父视图
this.child_count = _child.count;
cx.notify();
}).detach();
}
订阅全局状态
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
cx.observe_global::<AppTheme>(|this, cx| {
this.theme = cx.global::<AppTheme>().clone();
cx.notify();
}).detach();
}
⚠️ **注意**:订阅返回的 `Subscription` 必须 `.detach()` 或在字段中保持,否则订阅会被立即取消。详见 [第 8 章 · 订阅与取消](../08-lifecycle/subscriptions.md)。
4.5.8 状态管理的最佳实践
1. 单一职责
每个 ViewModel 只管理一个视图的状态:
// ✅ 单一职责
#[derive(IModel)]
#[component]
pub struct UserListView {
pub users: Vec<User>,
pub selected_user: Option<u64>,
}
#[derive(IModel)]
#[component]
pub struct UserDetailView {
pub user: User,
pub is_editing: bool,
}
// ❌ 上帝 ViewModel
#[derive(IModel)]
#[component]
pub struct MegaView {
pub users: Vec<User>,
pub selected_user: Option<u64>,
pub user_detail: User,
pub is_editing: bool,
pub posts: Vec<Post>,
pub comments: Vec<Comment>,
// ... 过多职责
}
2. 状态最小化
只保存必要的状态,能计算的用 `#[computed]`:
// ✅ 最小状态 + 计算属性
#[derive(IModel)]
#[component]
pub struct CartView {
pub items: Vec<CartItem>, // 唯一状态
}
impl CartView {
#[computed]
pub fn total_price(&self) -> f64 {
self.items.iter().map(|i| i.price * i.quantity as f64).sum()
}
#[computed]
pub fn total_count(&self) -> u32 {
self.items.iter().map(|i| i.quantity).sum()
}
}
// ❌ 冗余状态
#[derive(IModel)]
#[component]
pub struct BadCartView {
pub items: Vec<CartItem>,
pub total_price: f64, // 冗余,可计算
pub total_count: u32, // 冗余,可计算
}
3. 不可变优先
尽量用不可变字段,通过命令方法修改:
// ✅ 字段 pub 但通过命令修改
#[derive(IModel)]
#[component]
pub struct Counter {
pub count: i32,
}
impl Counter {
#[command]
pub fn increment(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
cx.notify();
}
}
4. 避免深度嵌套
// ❌ 深度嵌套,难以维护
#[derive(IModel)]
#[component]
pub struct DeepView {
pub data: Outer,
}
pub struct Outer { pub middle: Middle }
pub struct Middle { pub inner: Inner }
pub struct Inner { pub value: i32 }
// .rml: {data.middle.inner.value}
// ✅ 扁平化结构
#[derive(IModel)]
#[component]
pub struct FlatView {
pub value: i32,
}
4.5.9 状态管理反模式
反模式一:忘记 notify
// ❌ UI 不更新
#[command]
pub fn bad_update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
// 忘记 cx.notify()
}
反模式二:在异步中直接修改 self
// ❌ 编译错误或运行时错误
#[command]
pub fn bad_async(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
cx.spawn(|this, mut cx| async move {
// 不能直接捕获 &mut self
self.count += 1; // 编译错误
}).detach();
}
// ✅ 通过 this.update
#[command]
pub fn good_async(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
cx.spawn(|this, mut cx| async move {
let _ = this.update(&mut cx, |this, cx| {
this.count += 1;
cx.notify();
});
}).detach();
}
反模式三:过度使用全局状态
// ❌ 所有状态都放全局
#[derive(Global)]
pub struct GlobalState {
pub user: User,
pub cart: Vec<CartItem>,
pub theme: AppTheme,
pub settings: Settings,
// ... 过多状态
}
// ✅ 全局只放真正共享的状态
#[derive(Global)]
pub struct AppTheme { ... } // 只放主题
// 其他状态放在各自的 ViewModel 中
4.5.10 小结
RML 的状态管理核心:
- **状态即字段**:ViewModel 的 `pub` 字段就是状态 - **`cx.notify()`**:修改状态后必须调用,触发重绘 - **Entity 模型**:ViewModel 是 GPUI Entity,可被引用 - **跨视图共享**:父子引用、全局状态、上下文三种方式 - **异步更新**:通过 `this.update` 在异步任务中修改状态
最佳实践:单一职责、状态最小化、不可变优先、避免深度嵌套。
下一章 → [第 5 章 · 事件系统](../05-events/INDEX.md)