name: robius-state-management description: | 关键:用于Robius状态管理模式。触发条件: AppState、持久化、主题切换、状态管理、 Scope::with_data、保存状态、加载状态、serde、 状态持久化、主题切换
Robius 状态管理技能
基于Robrix和Moly代码库,在Makepad应用中实现状态管理和持久化的最佳实践。
源代码库:
- Robrix:Matrix聊天客户端 - 使用AppState、SelectedRoom,通过serde实现持久化
- Moly:AI聊天应用 - 中央存储模式、异步初始化、Preferences
触发条件
在以下情况使用此技能:
- 设计应用状态结构
- 实现状态持久化
- 在组件树中传递状态
- 跨会话管理UI状态
- 关键词:app state、makepad state、persistence、Scope::with_data、save state、load state
生产模式
有关生产就绪的状态管理模式,请参见 _base/ 目录:
| 模式 | 描述 |
|---|---|
| 06-global-registry | 使用Cx::set_global的全局组件注册表 |
| 07-radio-navigation | 使用单选按钮的标签式导航 |
| 10-state-machine | 基于枚举的状态机组件 |
| 11-theme-switching | 使用apply_over的多主题支持 |
| 12-local-persistence | 保存/加载用户偏好设置 |
AppState 结构
核心状态定义
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use matrix_sdk::ruma::OwnedRoomId;
/// 应用全局状态,跨多次应用运行持久化存储,并在应用各部分共享/更新。
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct AppState {
/// 当前选中的房间
pub selected_room: Option<SelectedRoom>,
/// 主视图的保存UI布局状态
pub saved_layout_state: SavedLayoutState,
/// 每个项目的保存状态(例如,每个空间的停靠布局)
pub saved_state_per_item: HashMap<OwnedRoomId, SavedLayoutState>,
/// 用户当前是否登录
#[serde(skip)] // 不持久化登录状态
pub logged_in: bool,
}
/// 表示当前选中的项目
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SelectedRoom {
JoinedRoom { room_name_id: RoomNameId },
InvitedRoom { room_name_id: RoomNameId },
Space { space_name_id: RoomNameId },
}
impl SelectedRoom {
pub fn room_id(&self) -> &OwnedRoomId {
match self {
Self::JoinedRoom { room_name_id } => room_name_id.room_id(),
Self::InvitedRoom { room_name_id } => room_name_id.room_id(),
Self::Space { space_name_id } => space_name_id.room_id(),
}
}
/// 从邀请状态升级到加入状态
pub fn upgrade_invite_to_joined(&mut self, room_id: &RoomId) -> bool {
match self {
Self::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => {
let name = room_name_id.clone();
*self = Self::JoinedRoom { room_name_id: name };
true
}
_ => false,
}
}
}
// 基于room_id的相等性
impl PartialEq for SelectedRoom {
fn eq(&self, other: &Self) -> bool {
self.room_id() == other.room_id()
}
}
impl Eq for SelectedRoom {}
布局/停靠状态持久化
/// UI布局状态的快照,用于恢复
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SavedLayoutState {
/// 布局中包含的所有项目,按键为ID
pub layout_items: HashMap<LiveIdSerde, LayoutItemSerde>,
/// 当前打开的项目,按键为ID
pub open_items: HashMap<LiveIdSerde, SelectedRoom>,
/// 项目打开的先后顺序(时间顺序)
pub item_order: Vec<SelectedRoom>,
/// 保存状态时当前选中的项目
pub selected_item: Option<SelectedRoom>,
}
/// LiveId的可序列化包装器
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct LiveIdSerde(pub u64);
impl From<LiveId> for LiveIdSerde {
fn from(id: LiveId) -> Self {
Self(id.0)
}
}
impl From<LiveIdSerde> for LiveId {
fn from(s: LiveIdSerde) -> Self {
LiveId(s.0)
}
}
通过Scope传播状态
在组件树中传递状态
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
// 转发到MatchEvent
self.match_event(cx, event);
// 创建带有AppState数据的Scope
let scope = &mut Scope::with_data(&mut self.app_state);
// 传递到组件树 - 所有子组件都可以访问AppState
self.ui.handle_event(cx, event, scope);
}
}
在子组件中访问状态
impl Widget for RoomScreen {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
// 从scope访问AppState
if let Some(app_state) = scope.data.get::<AppState>() {
if let Some(selected) = &app_state.selected_room {
self.update_for_room(cx, selected);
}
}
self.view.handle_event(cx, event, scope);
}
}
修改状态
impl Widget for RoomsList {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
// 对AppState的可变访问
if let Some(app_state) = scope.data.get_mut::<AppState>() {
if self.selection_changed {
app_state.selected_room = self.get_selected();
}
}
}
}
持久化层
文件路径
use std::path::{Path, PathBuf};
const LATEST_APP_STATE_FILE_NAME: &str = "latest_app_state.json";
const WINDOW_GEOM_STATE_FILE_NAME: &str = "window_geom_state.json";
/// 获取用户特定的持久化状态目录
fn persistent_state_dir(user_id: &UserId) -> PathBuf {
app_data_dir()
.join("users")
.join(user_id.to_string().replace(':', "_"))
}
/// 获取应用全局数据目录
fn app_data_dir() -> &'static Path {
// 平台特定的应用数据位置
static APP_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
APP_DATA_DIR.get_or_init(|| {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("myapp")
})
}
保存状态
use std::io::Write;
pub fn save_app_state(
app_state: AppState,
user_id: OwnedUserId,
) -> anyhow::Result<()> {
let file = std::fs::File::create(
persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME)
)?;
let mut writer = std::io::BufWriter::new(file);
serde_json::to_writer(&mut writer, &app_state)?;
writer.flush()?;
log!("成功将应用状态保存到持久化存储。");
Ok(())
}
/// 保存窗口几何状态(用户无关)
pub fn save_window_state(window_ref: WindowRef, cx: &Cx) -> anyhow::Result<()> {
let inner_size = window_ref.get_inner_size(cx);
let position = window_ref.get_position(cx);
let window_geom = WindowGeomState {
inner_size: (inner_size.x, inner_size.y),
position: (position.x, position.y),
is_fullscreen: window_ref.is_fullscreen(cx),
};
std::fs::write(
app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME),
serde_json::to_string(&window_geom)?,
)?;
Ok(())
}
加载状态
/// 加载应用状态,具有优雅回退
pub async fn load_app_state(user_id: &UserId) -> anyhow::Result<AppState> {
let state_path = persistent_state_dir(user_id).join(LATEST_APP_STATE_FILE_NAME);
// 读取文件
let file_bytes = match tokio::fs::read(&state_path).await {
Ok(fb) => fb,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
log!("未找到保存的应用状态,使用默认。");
return Ok(AppState::default());
}
Err(e) => return Err(e.into()),
};
// 反序列化并回退
match serde_json::from_slice(&file_bytes) {
Ok(app_state) => {
log!("成功加载应用状态。");
Ok(app_state)
}
Err(e) => {
error!("反序列化失败: {e}。可能格式不兼容。");
// 备份旧文件
let backup_path = state_path.with_extension("json.bak");
if let Err(backup_err) = tokio::fs::rename(&state_path, &backup_path).await {
error!("备份旧状态失败: {}", backup_err);
} else {
log!("旧状态备份到: {:?}", backup_path);
}
log!("使用默认应用状态。");
Ok(AppState::default())
}
}
}
/// 加载窗口几何状态(同步,在UI线程上)
pub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result<()> {
let file = match std::fs::File::open(app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME)) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e.into()),
};
let window_geom: WindowGeomState = serde_json::from_reader(file)?;
log!("恢复窗口几何: {window_geom:?}");
window_ref.configure_window(
cx,
dvec2(window_geom.inner_size.0, window_geom.inner_size.1),
dvec2(window_geom.position.0, window_geom.position.1),
window_geom.is_fullscreen,
"MyApp".to_string(),
);
Ok(())
}
启动/关闭集成
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
// 加载窗口几何状态(同步,在UI线程上)
if let Err(e) = persistence::load_window_state(
self.ui.window(ids!(main_window)), cx
) {
error!("加载窗口状态失败: {}", e);
}
// 触发异步应用状态加载
let user_id = get_current_user_id();
tokio::spawn(async move {
match persistence::load_app_state(&user_id).await {
Ok(app_state) => {
Cx::post_action(AppStateAction::RestoreFromPersistence(app_state));
SignalToUI::set_ui_signal();
}
Err(e) => error!("加载应用状态失败: {}", e),
}
});
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
if let Event::Shutdown = event {
// 保存窗口状态(同步)
if let Err(e) = persistence::save_window_state(
self.ui.window(ids!(main_window)), cx
) {
error!("保存窗口状态失败: {e}");
}
// 保存应用状态(同步)
if let Some(user_id) = current_user_id() {
if let Err(e) = persistence::save_app_state(
self.app_state.clone(), user_id
) {
error!("保存应用状态失败: {e}");
}
}
}
// ...
}
}
线程本地状态(仅UI)
use std::{cell::RefCell, rc::Rc, collections::HashMap};
thread_local! {
/// 仅UI线程缓存
static UI_CACHE: Rc<RefCell<HashMap<OwnedRoomId, CachedData>>> =
Rc::new(RefCell::new(HashMap::new()));
}
/// 获取缓存引用(需要Cx以确保在UI线程)
pub fn get_ui_cache(_cx: &mut Cx) -> Rc<RefCell<HashMap<OwnedRoomId, CachedData>>> {
UI_CACHE.with(Rc::clone)
}
/// 清除缓存(需要Cx)
pub fn clear_ui_cache(_cx: &mut Cx) {
UI_CACHE.with(|cache| cache.borrow_mut().clear());
}
最佳实践
- 分离持久化与运行时状态:对非持久化字段使用
#[serde(skip)] - 使用Scope::with_data()进行树传播:不要通过组件引用传递状态
- 优雅的反序列化回退:处理版本间的格式变化
- 备份旧状态文件:当格式变化时保留用户数据
- 用户特定的持久化路径:按用户账户分隔状态
- 同步窗口状态,异步应用状态:窗口几何在UI线程上同步加载
- 线程本地用于仅UI缓存:使用
thread_local!和 Cx 参数保护
参考文件
references/persistence-patterns.md- 额外的持久化模式(Robrix)references/state-structures.md- 状态结构示例(Robrix)references/moly-state-patterns.md- Moly特定模式- 包含所有状态的中央存储结构
- 使用
load_into_app()的异步存储初始化 - 应用状态检查模式(如果未加载则早期返回)
- 子模块状态管理器(搜索、下载、聊天)
- 提供者同步状态跟踪
- 存储操作转发到子模块