Implement Makepad App State Management
A skill for Makepad app state management and persistence, based on the Robrix and Moly codebases - AppState, Scope, serde-based save/load.
Why it matters
Establish robust state management and persistence patterns for Makepad applications, drawing from proven Robrix and Moly codebases. Ensure seamless state handling across UI components and sessions.
Outcomes
What it gets done
Design and implement application state structures.
Integrate state persistence mechanisms for saving and loading data.
Manage UI state propagation through the widget tree.
Handle state updates and modifications within child widgets.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-robius-state-management | bash Overview
Robius State Management Skill
Robius State Management provides Rust patterns for Makepad app state - a serializable AppState struct, Scope-based propagation through the widget tree, and async save/load with graceful fallback on missing or corrupted state files. Use it when designing state structure or persistence for a Makepad application; not for state management in other Rust UI frameworks.
What it does
Robius State Management is a skill covering best practices for state management and persistence in Makepad applications, drawn from two real codebases: Robrix (a Matrix chat client, contributing the AppState/SelectedRoom pattern and serde-based persistence) and Moly (an AI chat application, contributing the Central Store pattern, async initialization, and Preferences). It points to a _base/ directory of production-ready patterns including global widget registry via Cx::set_global, radio-button tab navigation, enum-based state machine widgets, multi-theme switching via apply_over, and local persistence of user preferences.
It defines a core AppState struct (derived Clone, Default, Debug, Serialize, Deserialize) holding the currently selected room, saved UI layout state, per-item saved states keyed by room ID, and a #[serde(skip)]-annotated login flag that is deliberately excluded from persistence. A SelectedRoom enum models joined rooms, invited rooms, and spaces, with an upgrade_invite_to_joined method and custom PartialEq/Eq based on room ID only. A SavedLayoutState struct captures layout items, open items, item order, and the selected item at save time, using a LiveIdSerde wrapper to make Makepad's LiveId serializable.
State propagates through the widget tree via Scope::with_data(&mut self.app_state) passed into self.ui.handle_event, with child widgets reading state via scope.data.get::<AppState>() and mutating it via scope.data.get_mut::<AppState>().
The persistence layer defines platform-specific data directory resolution (a OnceLock-cached app_data_dir() plus a per-user persistent_state_dir()), synchronous save functions using serde_json::to_writer with explicit flush(), and an async load_app_state function with graceful fallback: missing file returns AppState::default(), and a deserialize failure backs up the corrupted file to a .json.bak path before falling back to defaults rather than crashing. Window geometry (size, position, fullscreen state) is saved/loaded separately and synchronously since it must be available on the UI thread before app state may finish loading asynchronously.
When to use - and when NOT to
Use this skill when designing application state structure for a Makepad app, implementing state persistence to disk, passing state through the widget tree via Scope, or managing UI state that must survive across app sessions.
Do not use it for state management in non-Makepad UI frameworks - the Scope/Cx/LiveId patterns are specific to Makepad's architecture and won't transfer directly to other Rust UI toolkits.
Inputs and outputs
Input: the application's state shape (what needs to persist vs. session-only) and where it must be accessible in the widget tree.
Output: a serializable state struct plus save/load functions, for example the async load with graceful fallback:
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) => Ok(app_state),
Err(e) => {
error!("Failed to deserialize: {e}. May be incompatible format.");
let backup_path = state_path.with_extension("json.bak");
let _ = tokio::fs::rename(&state_path, &backup_path).await;
Ok(AppState::default())
}
}
}
Along with this it produces Scope-based state propagation code and window geometry persistence.
Who it's for
Rust developers building Makepad applications who need a proven state management and persistence architecture rather than designing app-wide state and save/load logic from scratch.
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.