Mobile Development · Guide

Building the UI

Declarative interfaces with Compose and SwiftUI, navigation that survives process death, and screens that fit every device and every user.

— min read Mobile Development

Describe the Screen, Not the Steps

Both platforms moved to declarative UI for the same reason: telling the framework what the screen should look like for a given state is far less error-prone than telling it which widgets to mutate when something changes.

The old model was imperative — find a view, set a property, remember to undo it later. Every new state multiplied the transitions you had to get right, and the bugs were always the same: a label that never got cleared, a spinner that never got hidden.

Declarative UI inverts it. You write a function from state to interface. When state changes the framework re-runs that function and works out the minimal update itself. The whole skill becomes modelling state well, because the UI is then a consequence rather than a thing you maintain.

Jetpack Compose

Compose is Android's declarative toolkit. A composable is a function annotated @Composable that emits UI; when the state it reads changes, Compose recomposes just that part.

@Composable
fun Counter(viewModel: CounterViewModel) {
    val count by viewModel.count.collectAsStateWithLifecycle()

    Column(Modifier.padding(16.dp)) {
        Text("Tapped $count times", style = MaterialTheme.typography.titleMedium)
        Button(onClick = viewModel::increment) { Text("Tap me") }
    }
}

Two rules carry most of the value. Hoist state: a composable that owns its own state cannot be reused or tested, so pass state down and events up. And keep composables side-effect free — they can run often and in any order, so anything that touches the world outside belongs in a documented effect API, not in the function body.

Reading a frequently-changing value at the top of a large composable recomposes the whole subtree. Read it as deeply as possible, in the smallest composable that needs it.

SwiftUI

SwiftUI is the same idea in Apple's dialect: a View is a struct with a body that describes the interface, and property wrappers declare where its state lives.

struct CounterView: View {
    @StateObject private var model = CounterModel()

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Tapped \(model.count) times").font(.headline)
            Button("Tap me") { model.increment() }
        }
        .padding()
    }
}
WrapperUse for
@StateSimple value state owned by this view
@BindingState owned by a parent, written by a child
@StateObjectA reference model this view creates and owns
@ObservedObjectA reference model handed in from elsewhere
@EnvironmentValues passed implicitly down the tree

Choosing the wrong wrapper is the most common SwiftUI bug: a model created as @ObservedObject is recreated on every redraw, so its state silently resets.

Screen Sizes & Orientation

"Mobile" spans a 5-inch phone, a folding device with two aspect ratios, a tablet in split-screen, and a desktop window on some platforms. Layouts that assume a width break on all of them.

Design against size classes rather than device names: compact width gets a single column and a bottom bar, expanded width gets a list-detail split. Both platforms expose this directly — window size classes on Android, size classes and NavigationSplitView on iOS.

Rotation and split-screen resize the window rather than restarting the app. Layout has to respond continuously, and any state tied to a specific width will be wrong within a frame.

Respect the safe area too: notches, dynamic islands, home indicators and rounded corners all eat into the frame, and content placed under them is either invisible or untappable.

Accessibility

Mobile accessibility is not an afterthought layer — it is the same content described properly. Both platforms ship a screen reader (TalkBack, VoiceOver), and both let users scale text far beyond the default.

RequirementIn practice
Labels on controlsAn icon-only button with no label is unusable with a screen reader
Touch targetsAt least 44–48dp; smaller is a miss for most thumbs, not just some
Dynamic typeLayouts must survive 200% text — no fixed heights around text
Contrast4.5:1 for body text, and never colour as the only signal
Reduce motionHonour the system setting; parallax makes some users ill

Test it the way users experience it: turn the screen reader on and navigate your main flow without looking. It takes ten minutes and finds more than any checklist.

Interview Questions

Why did both platforms move to declarative UI?

Imperative UI requires you to write every transition between every pair of states, and the bugs are always missed transitions. Declaring UI as a function of state makes the framework compute the difference instead.

What is state hoisting and why does it matter?

Moving state out of a component so it takes state as a parameter and emits events upward. It makes the component reusable, previewable and testable, and puts a single source of truth above it.

Why must composables be side-effect free?

Recomposition can run them often, in any order, and skip them entirely. Anything touching the outside world needs a lifecycle-aware effect API so it runs a predictable number of times.

Why pass ids rather than objects in navigation?

After process death the system restores navigation arguments but not your objects. An id can be re-fetched; a serialized object graph is fragile and often large.

How should a layout adapt across devices?

By window size class rather than device type — compact width gets one column, expanded gets list-detail — because folding, split-screen and rotation all change width without changing device.

What is the minimum accessibility bar on mobile?

Labels on every control, 44–48dp touch targets, layouts that survive 200% text scaling, 4.5:1 contrast, and honouring reduce-motion. Then navigate the main flow with the screen reader on.

Quick Quiz

1. In declarative UI, the interface is…
2. State hoisting means…
3. A SwiftUI model created with @ObservedObject instead of @StateObject will…
4. Navigation arguments should carry…
5. The minimum comfortable touch target is about…