Building the UI
Declarative interfaces with Compose and SwiftUI, navigation that survives process death, and screens that fit every device and every user.
Describe the Screen, Not the Steps
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.
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()
}
}
| Wrapper | Use for |
|---|---|
@State | Simple value state owned by this view |
@Binding | State owned by a parent, written by a child |
@StateObject | A reference model this view creates and owns |
@ObservedObject | A reference model handed in from elsewhere |
@Environment | Values 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.
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.
| Requirement | In practice |
|---|---|
| Labels on controls | An icon-only button with no label is unusable with a screen reader |
| Touch targets | At least 44–48dp; smaller is a miss for most thumbs, not just some |
| Dynamic type | Layouts must survive 200% text — no fixed heights around text |
| Contrast | 4.5:1 for body text, and never colour as the only signal |
| Reduce motion | Honour 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.