| name | robius-state-management |
| description | ALWAYS use this when the request matches Robius State Management: CRITICAL: Use for Robius state management patterns. |
Robius State Management Skill
Selective Reading Rule
Start with:
references/senior-master-standard.md
references/usage-routing.md
references/quality-checklist.md
Then load only the inherited docs, scripts, assets, or examples that match the user's actual task.
Best practices for state management and persistence in Makepad applications based on Robrix and Moly codebases.
Source codebases:
- Robrix: Matrix chat client - AppState, SelectedRoom, persistence via serde
- Moly: AI chat application - Central Store pattern, async initialization, Preferences
When to Use
Use this skill when:
- Designing application state structure
- Implementing state persistence
- Passing state through widget tree
- Managing UI state across sessions
- Keywords: app state, makepad state, persistence, Scope::with_data, save state, load state
Production Patterns
For production-ready state management patterns, see the _base/ directory:
| Pattern | Description |
|---|
| 06-global-registry | Global widget registry with Cx::set_global |
| 07-radio-navigation | Tab-style navigation with radio buttons |
| 10-state-machine | Enum-based state machine widgets |
| 11-theme-switching | Multi-theme support with apply_over |
| 12-local-persistence | Save/load user preferences |
AppState Structure
Core State Definition
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>,
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: &RoomId) {
{
::InvitedRoom { room_name_id } room_name_id.() == room_id => {
= room_name_id.();
* = ::JoinedRoom { room_name_id: name };
}
_ => ,
}
}
}
{
(&, other: &) {
.() == other.()
}
}
{}
Layout/Dock State Persistence
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct SavedLayoutState {
pub layout_items: HashMap<LiveIdSerde, LayoutItemSerde>,
pub open_items: HashMap<LiveIdSerde, SelectedRoom>,
pub item_order: Vec<SelectedRoom>,
pub selected_item: Option<SelectedRoom>,
}
#[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)
}
}
State Propagation via Scope
Passing State Through Widget Tree
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
self.match_event(cx, event);
let scope = &mut Scope::with_data(&mut self.app_state);
self.ui.handle_event(cx, event, scope);
}
}
Accessing State in Child Widgets
impl Widget for RoomScreen {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
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);
}
}
Modifying State
impl Widget for RoomsList {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if let Some(app_state) = scope.data.get_mut::<AppState>() {
if self.selection_changed {
app_state.selected_room = self.get_selected();
}
}
}
}
Persistence Layer
File Paths
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")
})
}
Saving State
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!("Successfully saved app state to persistent storage.");
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(())
}
Loading State
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!("No saved app state found, using default.");
return Ok(AppState::default());
}
Err(e) => return Err(e.into()),
};
match serde_json::from_slice(&file_bytes) {
Ok(app_state) => {
log!("Successfully loaded app state.");
Ok(app_state)
}
Err(e) => {
error!("Failed to deserialize: {e}. May be incompatible format.");
let backup_path = state_path.with_extension();
(backup_err) = tokio::fs::(&state_path, &backup_path). {
error!(, backup_err);
} {
log!(, backup_path);
}
log!();
(AppState::())
}
}
}
(window_ref: WindowRef, cx: & Cx) anyhow::<()> {
= std::fs::File::(().(WINDOW_GEOM_STATE_FILE_NAME)) {
(file) => file,
(e) e.() == std::io::ErrorKind::NotFound => (()),
(e) => (e.()),
};
: WindowGeomState = serde_json::(file)?;
log!();
window_ref.(
cx,
(window_geom.inner_size., window_geom.inner_size.),
(window_geom.position., window_geom.position.),
window_geom.is_fullscreen,
.(),
);
(())
}
Startup/Shutdown Integration
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
if let Err(e) = persistence::load_window_state(
self.ui.window(ids!(main_window)), cx
) {
error!("Failed to load window state: {}", 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!("Failed to load app state: {}", e),
}
});
}
}
impl AppMain for App {
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
if ::Shutdown = event {
(e) = persistence::(
.ui.(ids!(main_window)), cx
) {
error!();
}
(user_id) = () {
(e) = persistence::(
.app_state.(), user_id
) {
error!();
}
}
}
}
}
Thread-Local State (UI-Only)
use std::{cell::RefCell, rc::Rc, collections::HashMap};
thread_local! {
static UI_CACHE: Rc<RefCell<HashMap<OwnedRoomId, CachedData>>> =
Rc::new(RefCell::new(HashMap::new()));
}
pub fn get_ui_cache(_cx: &mut Cx) -> Rc<RefCell<HashMap<OwnedRoomId, CachedData>>> {
UI_CACHE.with(Rc::clone)
}
pub fn clear_ui_cache(_cx: &mut Cx) {
UI_CACHE.with(|cache| cache.borrow_mut().clear());
}
Best Practices
- Separate persistent vs runtime state: Use
#[serde(skip)] for non-persistent fields
- Use Scope::with_data() for tree propagation: Don't pass state through widget refs
- Graceful deserialization fallback: Handle format changes between versions
- Backup old state files: Preserve user data when format changes
- User-specific persistent paths: Separate state per user account
- Sync window state, async app state: Window geometry loads sync on UI thread
- Thread-local for UI-only caches: Use
thread_local! with Cx parameter guard
Reference Files
references/persistence-patterns.md - Additional persistence patterns (Robrix)
references/state-structures.md - State structure examples (Robrix)
references/moly-state-patterns.md - Moly-specific patterns
- Central Store struct containing all state
- Async Store initialization with
load_into_app()
- App state check pattern (early return if not loaded)
- Submodule state managers (Search, Downloads, Chats)
- Provider syncing status tracking
- Store action forwarding to submodules
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.