home / skills / zhanghandong / makepad-skills / robius-app-architecture

robius-app-architecture skill

/skills/robius-app-architecture

This skill guides structuring Makepad apps with Robius patterns, enabling async backend integration, signal-driven UI updates, and Tokio-runtime orchestration.

npx playbooks add skill zhanghandong/makepad-skills --skill robius-app-architecture

Review the files below or copy the command above to add this skill to your agents.

Files (8)
SKILL.md
12.0 KB
---
name: robius-app-architecture
description: |
  CRITICAL: Use for Robius app architecture patterns. Triggers on:
  Tokio, async, submit_async_request, 异步, 架构,
  SignalToUI, Cx::post_action, worker task,
  app structure, MatchEvent, handle_startup
---

# Robius App Architecture Skill

Best practices for structuring Makepad applications based on the Robrix and Moly codebases - production applications built with Makepad and Robius framework.

**Source codebases:**
- **Robrix**: Matrix chat client - complex sync/async with background subscriptions
- **Moly**: AI chat application - cross-platform (native + WASM) with streaming APIs

## Triggers

Use this skill when:
- Building a Makepad application with async backend integration
- Designing sync/async communication patterns in Makepad
- Structuring a Robius-style application
- Keywords: robrix, robius, makepad app structure, async makepad, tokio makepad

## Production Patterns

For production-ready async patterns, see the `_base/` directory:

| Pattern | Description |
|---------|-------------|
| [08-async-loading](./_base/08-async-loading.md) | Async data loading with loading states |
| [09-streaming-results](./_base/09-streaming-results.md) | Incremental results with SignalToUI |
| [13-tokio-integration](./_base/13-tokio-integration.md) | Full tokio runtime integration |

## Core Architecture Pattern

```
┌─────────────────────────────────────────────────────────────┐
│                     UI Thread (Makepad)                     │
│  ┌─────────┐     ┌──────────┐     ┌──────────────────────┐ │
│  │   App   │────▶│ WidgetRef │────▶│ Widget Tree (View)  │ │
│  │ State   │     │    ui     │     │ Scope::with_data()  │ │
│  └────┬────┘     └──────────┘     └──────────────────────┘ │
│       │                                                     │
│       │ submit_async_request()                              │
│       ▼                                                     │
│  ┌─────────────────┐          ┌─────────────────────────┐  │
│  │ REQUEST_SENDER  │─────────▶│  Crossbeam SegQueue     │  │
│  │ (MPSC Channel)  │          │  (Lock-free Updates)    │  │
│  └─────────────────┘          └─────────────────────────┘  │
└───────────────────────────────────┬─────────────────────────┘
                                    │
                    SignalToUI::set_ui_signal()
                                    │
┌───────────────────────────────────┴─────────────────────────┐
│                   Tokio Runtime (Async)                      │
│  ┌──────────────────────────────────────────────────────┐   │
│  │           worker_task (Request Handler)               │   │
│  │  - Receives Request from UI                           │   │
│  │  - Spawns async tasks per request                     │   │
│  │  - Posts actions back via Cx::post_action()           │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │         Per-Item Subscriber Tasks                     │   │
│  │  - Listens to external data stream                    │   │
│  │  - Sends Update via crossbeam channel                 │   │
│  │  - Calls SignalToUI::set_ui_signal() to wake UI       │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

## App Structure

### Top-Level App Definition

```rust
use makepad_widgets::*;

live_design! {
    use link::theme::*;
    use link::widgets::*;

    App = {{App}} {
        ui: <Root>{
            main_window = <Window> {
                window: {inner_size: vec2(1280, 800), title: "MyApp"},
                body = {
                    // Main content here
                }
            }
        }
    }
}

app_main!(App);

#[derive(Live)]
pub struct App {
    #[live] ui: WidgetRef,
    #[rust] app_state: AppState,
}

impl LiveRegister for App {
    fn live_register(cx: &mut Cx) {
        // Order matters: register base widgets first
        makepad_widgets::live_design(cx);
        // Then shared/common widgets
        crate::shared::live_design(cx);
        // Then feature modules
        crate::home::live_design(cx);
    }
}

impl LiveHook for App {
    fn after_new_from_doc(&mut self, cx: &mut Cx) {
        // One-time initialization after widget tree is created
    }
}
```

### AppMain Implementation

```rust
impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        // Forward to MatchEvent trait
        self.match_event(cx, event);

        // Pass AppState through widget tree via Scope
        let scope = &mut Scope::with_data(&mut self.app_state);
        self.ui.handle_event(cx, event, scope);
    }
}
```

## Tokio Runtime Integration

### Static Runtime Initialization

```rust
use std::sync::Mutex;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};

static TOKIO_RUNTIME: Mutex<Option<tokio::runtime::Runtime>> = Mutex::new(None);
static REQUEST_SENDER: Mutex<Option<UnboundedSender<AppRequest>>> = Mutex::new(None);

pub fn start_async_runtime() -> Result<tokio::runtime::Handle> {
    let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();

    let rt_handle = TOKIO_RUNTIME.lock().unwrap()
        .get_or_insert_with(|| {
            tokio::runtime::Runtime::new()
                .expect("Failed to create Tokio runtime")
        })
        .handle()
        .clone();

    // Store sender for UI thread to use
    *REQUEST_SENDER.lock().unwrap() = Some(request_sender);

    // Spawn the main worker task
    rt_handle.spawn(worker_task(request_receiver));

    Ok(rt_handle)
}
```

### Request Submission Pattern

```rust
pub enum AppRequest {
    FetchData { id: String },
    SendMessage { content: String },
    // ... other request types
}

/// Submit a request from UI thread to async runtime
pub fn submit_async_request(req: AppRequest) {
    if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() {
        sender.send(req)
            .expect("BUG: worker task receiver has died!");
    }
}
```

### Worker Task Pattern

```rust
async fn worker_task(mut request_receiver: UnboundedReceiver<AppRequest>) -> Result<()> {
    while let Some(request) = request_receiver.recv().await {
        match request {
            AppRequest::FetchData { id } => {
                // Spawn a new task for each request
                let _task = tokio::spawn(async move {
                    let result = fetch_data(&id).await;
                    // Post result back to UI thread
                    Cx::post_action(DataFetchedAction { id, result });
                });
            }
            AppRequest::SendMessage { content } => {
                let _task = tokio::spawn(async move {
                    match send_message(&content).await {
                        Ok(()) => Cx::post_action(MessageSentAction::Success),
                        Err(e) => Cx::post_action(MessageSentAction::Failed(e)),
                    }
                });
            }
        }
    }
    Ok(())
}
```

## Lock-Free Update Queue Pattern

For high-frequency updates from background tasks:

```rust
use crossbeam_queue::SegQueue;
use makepad_widgets::SignalToUI;

pub enum DataUpdate {
    NewItem { item: Item },
    ItemChanged { id: String, changes: Changes },
    Status { message: String },
}

static PENDING_UPDATES: SegQueue<DataUpdate> = SegQueue::new();

/// Called from background async tasks
pub fn enqueue_update(update: DataUpdate) {
    PENDING_UPDATES.push(update);
    SignalToUI::set_ui_signal();  // Wake UI thread
}

// In widget's handle_event:
impl Widget for MyWidget {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
        // Poll for updates on Signal events
        if let Event::Signal = event {
            while let Some(update) = PENDING_UPDATES.pop() {
                match update {
                    DataUpdate::NewItem { item } => {
                        self.items.push(item);
                        self.redraw(cx);
                    }
                    // ... handle other updates
                }
            }
        }
    }
}
```

## Startup Sequence

```rust
impl MatchEvent for App {
    fn handle_startup(&mut self, cx: &mut Cx) {
        // 1. Initialize logging
        let _ = tracing_subscriber::fmt::try_init();

        // 2. Initialize app data directory
        let _app_data_dir = crate::app_data_dir();

        // 3. Load persisted state
        if let Err(e) = persistence::load_window_state(
            self.ui.window(ids!(main_window)), cx
        ) {
            error!("Failed to load window state: {}", e);
        }

        // 4. Update UI based on loaded state
        self.update_ui_visibility(cx);

        // 5. Start async runtime
        let _rt_handle = crate::start_async_runtime().unwrap();
    }
}
```

## Shutdown Sequence

```rust
impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        if let Event::Shutdown = event {
            // Save window geometry
            let window_ref = self.ui.window(ids!(main_window));
            if let Err(e) = persistence::save_window_state(window_ref, cx) {
                error!("Failed to save window state: {e}");
            }

            // Save app state
            if let Some(user_id) = current_user_id() {
                if let Err(e) = persistence::save_app_state(
                    self.app_state.clone(), user_id
                ) {
                    error!("Failed to save app state: {e}");
                }
            }
        }
        // ... rest of event handling
    }
}
```

## Best Practices

1. **Separation of Concerns**: Keep UI logic on the main thread, async operations in Tokio runtime
2. **Request/Response Pattern**: Use typed enums for requests and actions
3. **Lock-Free Updates**: Use `crossbeam::SegQueue` for high-frequency background updates
4. **SignalToUI**: Always call `SignalToUI::set_ui_signal()` after enqueueing updates
5. **Cx::post_action()**: Use for async task results that need action handling
6. **Scope::with_data()**: Pass shared state through widget tree
7. **Module Registration Order**: Register base widgets before dependent modules in `live_register()`

## Reference Files

- `references/tokio-integration.md` - Detailed Tokio runtime patterns (Robrix)
- `references/channel-patterns.md` - Channel communication patterns (Robrix)
- `references/moly-async-patterns.md` - Cross-platform async patterns (Moly)
  - `PlatformSend` trait for native/WASM compatibility
  - `UiRunner` for async defer operations
  - `AbortOnDropHandle` for task cancellation
  - `ThreadToken` for non-Send types on WASM
  - `spawn()` platform-agnostic function

Overview

This skill documents Robius-style app architecture patterns for Makepad applications, focused on mixing synchronous UI code with a Tokio async runtime. It distills production-ready patterns for request submission, worker tasks, lock-free update queues, and startup/shutdown sequencing. Use it to design predictable, cross-platform (native + WASM) Makepad apps with robust background processing and UI wake-up semantics.

How this skill works

The skill describes how the UI thread submits typed AppRequest enums into a Tokio-backed request channel and how a worker_task spawns per-request async tasks. Background tasks enqueue DataUpdate items into a crossbeam SegQueue and call SignalToUI::set_ui_signal() so the main thread polls and applies updates inside widgets. It also covers static runtime initialization, Cx::post_action() usage for routed responses, and patterns for startup/shutdown state persistence.

When to use it

  • Building a Makepad app that needs background async I/O or streaming APIs
  • Designing clear UI ↔ async runtime communication using typed requests/actions
  • Handling high-frequency updates from network or streams without locking the UI thread
  • Implementing cross-platform async behavior compatible with native and WASM targets
  • Establishing reliable startup and shutdown sequences with persisted state

Best practices

  • Keep UI logic on the main thread and all blocking/async work in the Tokio runtime
  • Use typed enums (AppRequest, DataUpdate) for requests and updates to make flows explicit
  • Use crossbeam::SegQueue for lock-free, high-frequency background updates
  • Always call SignalToUI::set_ui_signal() after enqueueing updates to wake the UI thread
  • Use Cx::post_action() for async task results that need to be handled by the UI event loop
  • Register base widgets before dependent modules in live_register() and pass shared state via Scope::with_data()

Example use cases

  • Chat client with live sync: submit fetch or send requests from UI, stream incoming messages via subscribers
  • AI assistant with streaming results: spawn async streaming tasks and push incremental tokens via SegQueue + SignalToUI
  • Background sync service: worker_task listens for periodic tasks and posts state changes with Cx::post_action()
  • Cross-platform app: use PlatformSend/UiRunner patterns to unify native and WASM task spawning
  • High-frequency sensor or telemetry UI: enqueue frequent updates lock-free and apply them on Signal events

FAQ

How do I wake the UI when background data arrives?

Push a DataUpdate into the SegQueue and call SignalToUI::set_ui_signal(); on Event::Signal the UI should drain the queue and update state.

Where do I store the request sender for submit_async_request()?

Keep an Arc/Mutex-wrapped static or global (e.g., REQUEST_SENDER) initialized when starting the static Tokio runtime so the UI thread can send AppRequest values.