[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-12226e68-faba-411f-845e-68a9ba2f2301":3,"$fyhTINox-xTWIkTcCfQIfaCE2YOgwerxf3X-NmVC7GKI":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"12226e68-faba-411f-845e-68a9ba2f2301","robius-state-management","|","cat_life_career","mod_other","sickn33,other","---\nname: robius-state-management\ndescription: |\n  CRITICAL: Use for Robius state management patterns. Triggers on:\n  AppState, persistence, theme switch, 状态管理,\n  Scope::with_data, save state, load state, serde,\n  状态持久化, 主题切换\nrisk: unknown\nsource: community\n---\n\n# Robius State Management Skill\n\nBest practices for state management and persistence in Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix chat client - AppState, SelectedRoom, persistence via serde\n- **Moly**: AI chat application - Central Store pattern, async initialization, Preferences\n\n## When to Use\nUse this skill when:\n- Designing application state structure\n- Implementing state persistence\n- Passing state through widget tree\n- Managing UI state across sessions\n- Keywords: app state, makepad state, persistence, Scope::with_data, save state, load state\n\n## Production Patterns\n\nFor production-ready state management patterns, see the `_base\u002F` directory:\n\n| Pattern | Description |\n|---------|-------------|\n| 06-global-registry | Global widget registry with Cx::set_global |\n| 07-radio-navigation | Tab-style navigation with radio buttons |\n| 10-state-machine | Enum-based state machine widgets |\n| 11-theme-switching | Multi-theme support with apply_over |\n| 12-local-persistence | Save\u002Fload user preferences |\n\n## AppState Structure\n\n### Core State Definition\n\n```rust\nuse serde::{Serialize, Deserialize};\nuse std::collections::HashMap;\nuse matrix_sdk::ruma::OwnedRoomId;\n\n\u002F\u002F\u002F App-wide state that is stored persistently across multiple app runs\n\u002F\u002F\u002F and shared\u002Fupdated across various parts of the app.\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct AppState {\n    \u002F\u002F\u002F The currently-selected room\n    pub selected_room: Option\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Saved UI layout state for main view\n    pub saved_layout_state: SavedLayoutState,\n\n    \u002F\u002F\u002F Per-item saved states (e.g., per-space dock layouts)\n    pub saved_state_per_item: HashMap\u003COwnedRoomId, SavedLayoutState>,\n\n    \u002F\u002F\u002F Whether a user is currently logged in\n    #[serde(skip)]  \u002F\u002F Don't persist login state\n    pub logged_in: bool,\n}\n\n\u002F\u002F\u002F Represents a currently selected item\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub enum SelectedRoom {\n    JoinedRoom { room_name_id: RoomNameId },\n    InvitedRoom { room_name_id: RoomNameId },\n    Space { space_name_id: RoomNameId },\n}\n\nimpl SelectedRoom {\n    pub fn room_id(&self) -> &OwnedRoomId {\n        match self {\n            Self::JoinedRoom { room_name_id } => room_name_id.room_id(),\n            Self::InvitedRoom { room_name_id } => room_name_id.room_id(),\n            Self::Space { space_name_id } => space_name_id.room_id(),\n        }\n    }\n\n    \u002F\u002F\u002F Upgrade from invited to joined state\n    pub fn upgrade_invite_to_joined(&mut self, room_id: &RoomId) -> bool {\n        match self {\n            Self::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => {\n                let name = room_name_id.clone();\n                *self = Self::JoinedRoom { room_name_id: name };\n                true\n            }\n            _ => false,\n        }\n    }\n}\n\n\u002F\u002F Equality based on room_id only\nimpl PartialEq for SelectedRoom {\n    fn eq(&self, other: &Self) -> bool {\n        self.room_id() == other.room_id()\n    }\n}\nimpl Eq for SelectedRoom {}\n```\n\n### Layout\u002FDock State Persistence\n\n```rust\n\u002F\u002F\u002F A snapshot of UI layout state for restoration\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct SavedLayoutState {\n    \u002F\u002F\u002F All items contained in the layout, keyed by ID\n    pub layout_items: HashMap\u003CLiveIdSerde, LayoutItemSerde>,\n\n    \u002F\u002F\u002F Items currently open, keyed by ID\n    pub open_items: HashMap\u003CLiveIdSerde, SelectedRoom>,\n\n    \u002F\u002F\u002F Order items were opened (chronological)\n    pub item_order: Vec\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Currently selected item when state was saved\n    pub selected_item: Option\u003CSelectedRoom>,\n}\n\n\u002F\u002F\u002F Serializable wrapper for LiveId\n#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]\npub struct LiveIdSerde(pub u64);\n\nimpl From\u003CLiveId> for LiveIdSerde {\n    fn from(id: LiveId) -> Self {\n        Self(id.0)\n    }\n}\n\nimpl From\u003CLiveIdSerde> for LiveId {\n    fn from(s: LiveIdSerde) -> Self {\n        LiveId(s.0)\n    }\n}\n```\n\n## State Propagation via Scope\n\n### Passing State Through Widget Tree\n\n```rust\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        \u002F\u002F Forward to MatchEvent\n        self.match_event(cx, event);\n\n        \u002F\u002F Create Scope with AppState data\n        let scope = &mut Scope::with_data(&mut self.app_state);\n\n        \u002F\u002F Pass to widget tree - all children can access AppState\n        self.ui.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Accessing State in Child Widgets\n\n```rust\nimpl Widget for RoomScreen {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Access AppState from scope\n        if let Some(app_state) = scope.data.get::\u003CAppState>() {\n            if let Some(selected) = &app_state.selected_room {\n                self.update_for_room(cx, selected);\n            }\n        }\n\n        self.view.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Modifying State\n\n```rust\nimpl Widget for RoomsList {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Mutable access to AppState\n        if let Some(app_state) = scope.data.get_mut::\u003CAppState>() {\n            if self.selection_changed {\n                app_state.selected_room = self.get_selected();\n            }\n        }\n    }\n}\n```\n\n## Persistence Layer\n\n### File Paths\n\n```rust\nuse std::path::{Path, PathBuf};\n\nconst LATEST_APP_STATE_FILE_NAME: &str = \"latest_app_state.json\";\nconst WINDOW_GEOM_STATE_FILE_NAME: &str = \"window_geom_state.json\";\n\n\u002F\u002F\u002F Get user-specific persistent state directory\nfn persistent_state_dir(user_id: &UserId) -> PathBuf {\n    app_data_dir()\n        .join(\"users\")\n        .join(user_id.to_string().replace(':', \"_\"))\n}\n\n\u002F\u002F\u002F Get app-wide data directory\nfn app_data_dir() -> &'static Path {\n    \u002F\u002F Platform-specific app data location\n    static APP_DATA_DIR: OnceLock\u003CPathBuf> = OnceLock::new();\n    APP_DATA_DIR.get_or_init(|| {\n        dirs::data_dir()\n            .unwrap_or_else(|| PathBuf::from(\".\"))\n            .join(\"myapp\")\n    })\n}\n```\n\n### Saving State\n\n```rust\nuse std::io::Write;\n\npub fn save_app_state(\n    app_state: AppState,\n    user_id: OwnedUserId,\n) -> anyhow::Result\u003C()> {\n    let file = std::fs::File::create(\n        persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME)\n    )?;\n    let mut writer = std::io::BufWriter::new(file);\n    serde_json::to_writer(&mut writer, &app_state)?;\n    writer.flush()?;\n    log!(\"Successfully saved app state to persistent storage.\");\n    Ok(())\n}\n\n\u002F\u002F\u002F Save window geometry state (user-agnostic)\npub fn save_window_state(window_ref: WindowRef, cx: &Cx) -> anyhow::Result\u003C()> {\n    let inner_size = window_ref.get_inner_size(cx);\n    let position = window_ref.get_position(cx);\n    let window_geom = WindowGeomState {\n        inner_size: (inner_size.x, inner_size.y),\n        position: (position.x, position.y),\n        is_fullscreen: window_ref.is_fullscreen(cx),\n    };\n    std::fs::write(\n        app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME),\n        serde_json::to_string(&window_geom)?,\n    )?;\n    Ok(())\n}\n```\n\n### Loading State\n\n```rust\n\u002F\u002F\u002F Load app state with graceful fallback\npub async fn load_app_state(user_id: &UserId) -> anyhow::Result\u003CAppState> {\n    let state_path = persistent_state_dir(user_id).join(LATEST_APP_STATE_FILE_NAME);\n\n    \u002F\u002F Read file\n    let file_bytes = match tokio::fs::read(&state_path).await {\n        Ok(fb) => fb,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n            log!(\"No saved app state found, using default.\");\n            return Ok(AppState::default());\n        }\n        Err(e) => return Err(e.into()),\n    };\n\n    \u002F\u002F Deserialize with fallback\n    match serde_json::from_slice(&file_bytes) {\n        Ok(app_state) => {\n            log!(\"Successfully loaded app state.\");\n            Ok(app_state)\n        }\n        Err(e) => {\n            error!(\"Failed to deserialize: {e}. May be incompatible format.\");\n\n            \u002F\u002F Backup old file\n            let backup_path = state_path.with_extension(\"json.bak\");\n            if let Err(backup_err) = tokio::fs::rename(&state_path, &backup_path).await {\n                error!(\"Failed to backup old state: {}\", backup_err);\n            } else {\n                log!(\"Old state backed up to: {:?}\", backup_path);\n            }\n\n            log!(\"Using default app state.\");\n            Ok(AppState::default())\n        }\n    }\n}\n\n\u002F\u002F\u002F Load window geometry (synchronous, on UI thread)\npub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result\u003C()> {\n    let file = match std::fs::File::open(app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME)) {\n        Ok(file) => file,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(e) => return Err(e.into()),\n    };\n\n    let window_geom: WindowGeomState = serde_json::from_reader(file)?;\n    log!(\"Restoring window geometry: {window_geom:?}\");\n\n    window_ref.configure_window(\n        cx,\n        dvec2(window_geom.inner_size.0, window_geom.inner_size.1),\n        dvec2(window_geom.position.0, window_geom.position.1),\n        window_geom.is_fullscreen,\n        \"MyApp\".to_string(),\n    );\n    Ok(())\n}\n```\n\n### Startup\u002FShutdown Integration\n\n```rust\nimpl MatchEvent for App {\n    fn handle_startup(&mut self, cx: &mut Cx) {\n        \u002F\u002F Load window geometry (sync, on UI thread)\n        if let Err(e) = persistence::load_window_state(\n            self.ui.window(ids!(main_window)), cx\n        ) {\n            error!(\"Failed to load window state: {}\", e);\n        }\n\n        \u002F\u002F Trigger async app state load\n        let user_id = get_current_user_id();\n        tokio::spawn(async move {\n            match persistence::load_app_state(&user_id).await {\n                Ok(app_state) => {\n                    Cx::post_action(AppStateAction::RestoreFromPersistence(app_state));\n                    SignalToUI::set_ui_signal();\n                }\n                Err(e) => error!(\"Failed to load app state: {}\", e),\n            }\n        });\n    }\n}\n\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        if let Event::Shutdown = event {\n            \u002F\u002F Save window state (sync)\n            if let Err(e) = persistence::save_window_state(\n                self.ui.window(ids!(main_window)), cx\n            ) {\n                error!(\"Failed to save window state: {e}\");\n            }\n\n            \u002F\u002F Save app state (sync)\n            if let Some(user_id) = current_user_id() {\n                if let Err(e) = persistence::save_app_state(\n                    self.app_state.clone(), user_id\n                ) {\n                    error!(\"Failed to save app state: {e}\");\n                }\n            }\n        }\n        \u002F\u002F ...\n    }\n}\n```\n\n## Thread-Local State (UI-Only)\n\n```rust\nuse std::{cell::RefCell, rc::Rc, collections::HashMap};\n\nthread_local! {\n    \u002F\u002F\u002F UI-thread-only cache\n    static UI_CACHE: Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> =\n        Rc::new(RefCell::new(HashMap::new()));\n}\n\n\u002F\u002F\u002F Get cache reference (requires Cx to ensure UI thread)\npub fn get_ui_cache(_cx: &mut Cx) -> Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> {\n    UI_CACHE.with(Rc::clone)\n}\n\n\u002F\u002F\u002F Clear cache (requires Cx)\npub fn clear_ui_cache(_cx: &mut Cx) {\n    UI_CACHE.with(|cache| cache.borrow_mut().clear());\n}\n```\n\n## Best Practices\n\n1. **Separate persistent vs runtime state**: Use `#[serde(skip)]` for non-persistent fields\n2. **Use Scope::with_data() for tree propagation**: Don't pass state through widget refs\n3. **Graceful deserialization fallback**: Handle format changes between versions\n4. **Backup old state files**: Preserve user data when format changes\n5. **User-specific persistent paths**: Separate state per user account\n6. **Sync window state, async app state**: Window geometry loads sync on UI thread\n7. **Thread-local for UI-only caches**: Use `thread_local!` with Cx parameter guard\n\n## Reference Files\n\n- `references\u002Fpersistence-patterns.md` - Additional persistence patterns (Robrix)\n- `references\u002Fstate-structures.md` - State structure examples (Robrix)\n- `references\u002Fmoly-state-patterns.md` - Moly-specific patterns\n  - Central Store struct containing all state\n  - Async Store initialization with `load_into_app()`\n  - App state check pattern (early return if not loaded)\n  - Submodule state managers (Search, Downloads, Chats)\n  - Provider syncing status tracking\n  - Store action forwarding to submodules\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,67,863,"2026-05-16 13:37:20",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"其他","other","mdi-page-next-outline","其他类型Skill",5,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"职场发展","career","mdi-briefcase-outline","面试准备、简历优化、职业规划",4,575,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"4e53bd2a-db45-442d-83a5-dd4881d634b4","1.0.0","robius-state-management.zip",4589,"uploads\u002Fskills\u002F12226e68-faba-411f-845e-68a9ba2f2301\u002Frobius-state-management.zip","b977e2c197644c9158ba00cda9d5553501eaddd2a6277c8c750e3a14263d5635","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":13024}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]