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:
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
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:
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:
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:
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
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:
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:
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
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:
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:
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
| Event | When it fires |
|---|---|
when screen opens | Screen is first created — initial data load. |
when screen resumes | Screen comes back to the foreground (app foregrounded, or navigated back to). |
when screen pauses | Screen 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:
# 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
# main.zx
import auth
mobile app "MyApp"
screen Home
column padding 16
text "Welcome!" style heading
end
end
start with auth.Login
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
- Android Data & Storage — API consumption, local databases, caching, and sync patterns.
- Android UI Patterns — theming, widgets, forms, and accessibility.
- Mobile Tooling — debugging, testing, profiling, and Play Store publishing.