Zornux docs
Get started Spec

Mobile

Android Architecture

A real Android app has more than a few screens. This page covers the patterns that keep a growing app organized: shared state, navigation structures, error boundaries, lifecycle awareness, and multi-module projects.

Screen-local vs shared state

A state variable inside a screen is local — it resets when the user navigates away and comes back. For data that persists across screens, declare it at the app level:

zornux
mobile app "ShopApp"

state cart = []
state user = nothing

screen Products
    state products = []

    when screen opens
        products = http.get("https://api.example.com/products")
    end

    column scroll padding 16 spacing 8
        text "Products" style heading
        for each product in products
            card
                row padding 16 spacing 12 align center
                    column
                        text product.name style subheading
                        text "$" + product.price style caption
                    end
                    button "Add"
                        when tapped
                            add product to cart
                            show product.name + " added to cart"
                        end
                    end
                end
            end
        end
    end
end

screen Cart
    column scroll padding 16 spacing 8
        text "Cart (" + (length of cart) + " items)" style heading
        for each item in cart
            card
                row padding 16 align center
                    text item.name
                    spacer
                    text "$" + item.price
                end
            end
        end
    end
end

start with Products
App state is reactive

Shared state declared under mobile app is reactive across all screens. When the cart changes on one screen, any other screen rendering cart re-renders automatically.

Navigation patterns

Tab bar

A tabs block creates a bottom navigation bar — the standard pattern for top-level destinations:

zornux
mobile app "NewsApp"

tabs
    tab "Home" icon "home" screen Home
    tab "Search" icon "search" screen Search
    tab "Saved" icon "bookmark" screen Saved
    tab "Profile" icon "person" screen Profile
end

screen Home
    column padding 16
        text "Today's Headlines" style heading
    end
end

screen Search
    state query = ""
    column padding 16
        input query "Search articles..."
    end
end

screen Saved
    column padding 16
        text "Saved Articles" style heading
    end
end

screen Profile
    column padding 16
        text "Your Profile" style heading
    end
end

start with Home

Each tab keeps its own back stack — navigating within a tab does not affect the other tabs.

Navigation drawer

A drawer block creates a side navigation menu, accessed by swiping from the left edge or tapping a hamburger icon:

zornux
mobile app "AdminApp"

drawer
    item "Dashboard" icon "dashboard" screen Dashboard
    item "Users" icon "people" screen Users
    item "Settings" icon "settings" screen Settings
end

start with Dashboard

Deep linking

A screen can declare a link so external URLs or notifications open it directly:

zornux
screen ProductDetail receives productId
    link "/products/:productId"

    state product = nothing

    when screen opens
        product = http.get("https://api.example.com/products/" + productId)
    end

    column padding 16
        if product is not nothing
            text product.name style heading
            text product.description
            text "$" + product.price style subheading
        else
            progress
        end
    end
end
Test deep links locally

Run zornux mobile run android --deeplink "/products/42" to launch the app at a specific screen during development.

Loading and error states

Every screen that fetches data should handle three states: loading, loaded, and error. Zornux's try/catch works inside when screen opens:

zornux
screen UserProfile receives userId
    state user = nothing
    state loading = true
    state error_message = ""

    when screen opens
        try
            user = http.get("https://api.example.com/users/" + userId)
            loading = false
        catch error
            error_message = error.message
            loading = false
        end
    end

    column padding 24 spacing 16
        if loading
            progress
            text "Loading profile..." style caption
        else if error_message is not ""
            icon "error" label "Error"
            text error_message
            button "Retry"
                when tapped
                    loading = true
                    error_message = ""
                    try
                        user = http.get("https://api.example.com/users/" + userId)
                        loading = false
                    catch error
                        error_message = error.message
                        loading = false
                    end
                end
            end
        else
            text user.name style heading
            text user.email style caption
        end
    end
end

Reusable components

Extract repeated UI patterns into component blocks. A component is like a screen fragment — it takes parameters and returns a widget tree:

zornux
component UserCard with user
    card
        row padding 16 spacing 12 align center
            image user.avatar
                width 48
                height 48
                corner_radius 24
            end
            column spacing 4
                text user.name style subheading
                text user.role style caption
            end
            spacer
            icon "chevron_right"
        end
    end
end

screen TeamList
    state members = []

    when screen opens
        members = http.get("https://api.example.com/team")
    end

    column scroll padding 16 spacing 8
        text "Team" style heading
        for each member in members
            UserCard with member
                when tapped
                    go to UserProfile with member.id
                end
            end
        end
    end
end
Components are not screens

A component has no lifecycle, no state of its own, and no navigation entry. It renders inline wherever you place it. For persistent state, pass it down from the parent screen.

Pull-to-refresh

Add refreshable to a scrollable column to enable the standard Android pull-to-refresh gesture:

zornux
screen Feed
    state posts = []

    when screen opens
        posts = http.get("https://api.example.com/posts")
    end

    function refresh
        posts = http.get("https://api.example.com/posts")
    end

    column scroll refreshable padding 16 spacing 8
        for each post in posts
            card
                column padding 16
                    text post.title style subheading
                    text post.summary
                end
            end
        end
    end
end

Lifecycle awareness

Beyond when screen opens, screens respond to lifecycle events:

zornux
screen LiveDashboard
    state metrics = nothing

    when screen opens
        metrics = http.get("https://api.example.com/metrics")
    end

    when screen resumes
        metrics = http.get("https://api.example.com/metrics")
    end

    when screen pauses
        store.save("last_metrics", metrics)
    end

    column padding 16
        if metrics is not nothing
            text "Active users: " + metrics.active_users style heading
            text "Uptime: " + metrics.uptime
        end
    end
end
EventWhen it fires
when screen opensScreen is first created — initial data load.
when screen resumesScreen comes back to the foreground (app foregrounded, or navigated back to).
when screen pausesScreen is leaving the foreground — save state or stop timers.

Multi-module mobile projects

As an app grows, split it into modules — each module is a .zx file that exports screens, components, or functions:

zornux
# screens/auth.zx
module auth

screen Login
    state email = ""
    state password = ""

    column padding 24 spacing 16
        text "Sign In" style heading
        input email "Email"
        input password "Password" secure
        button "Sign In"
            when tapped
                create result = http.post("https://api.example.com/login")
                secure_storage.save("token", result.token)
                go to Home
            end
        end
    end
end

screen Register
    state name = ""
    state email = ""
    state password = ""

    column padding 24 spacing 16
        text "Create Account" style heading
        input name "Full name"
        input email "Email"
        input password "Password" secure
        button "Sign Up"
            when tapped
                http.post("https://api.example.com/register")
                go to Login
            end
        end
    end
end
zornux
# main.zx
import auth

mobile app "MyApp"

screen Home
    column padding 16
        text "Welcome!" style heading
    end
end

start with auth.Login
One module per feature

Group related screens into modules — auth.zx, orders.zx, settings.zx. This keeps each file focused and makes the project navigable without a directory tree.

What's next