CtrlK
BlogDocsLog inGet started
Tessl Logo

robius-app-architecture

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

50

Quality

55%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./skills/robius-app-architecture/SKILL.md
SKILL.md
Quality
Evals
Security

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:

PatternDescription
08-async-loadingAsync data loading with loading states
09-streaming-resultsIncremental results with SignalToUI
13-tokio-integrationFull 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

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

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

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

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

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:

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

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

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
Repository
administrakt0r/AI-Agents-Safe-Coding-Skills
Last updated
First committed

Also appears in

administrakt0r/AI-Agents-Safe-Coding-Skills
In sync

since Aug 19, 2026

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.