← All recipes

Recipe: Atomic Design, taken apart on the Order Status screen

Cross-platform: Kotlin Multiplatform, Flutter, React Native.

Problem

"Build the order screen" is how you end up with a 300-line widget that fetches, formats, branches on five states, and paints — untestable, unreusable, and impossible to preview. Components need a taxonomy so that each piece does one thing and every piece has a home.

Concept

Atoms → molecules → organisms → screens. Atoms hold no business logic (a Button, a StatusBadge, a PriceTag); organisms compose atoms into a meaningful block (the OrderStatusPanel); screens place organisms in a scaffold and wire them to state. State flows down as props; events flow up as callbacks. The Order Status screen is the clearest worked example, because it has a real state machine behind it (pending → paid | failed | refunded).

Implementation ×3

The same decomposition in each app: a screen that renders a UiState<Order>, an OrderStatusPanel organism that maps the order's status to a label, colour and message, and atoms (StatusBadge, money text, PrimaryButton) underneath.

KMPui/components/Components.kt, ui/screens/Screens.kt

@Composable
fun OrderStatusPanel(order: Order, onDone: () -> Unit) {
    val (label, color, message) = when (order.status) {
        OrderStatus.PENDING -> Triple("Pending", Tokens.warn, "Payment is processing…")
        OrderStatus.PAID -> Triple("Paid", Tokens.ok, "You're all set.")
        OrderStatus.FAILED -> Triple("Failed", Tokens.err, "Payment failed. Try again.")
        OrderStatus.REFUNDED -> Triple("Refunded", Tokens.muted, "This order was refunded.")
    }
    // StatusBadge(label, color) + money + a Done button when settled
}

OrderStatusScreen is just UiStateContent(state) { order -> OrderStatusPanel(order, onDone) }.

Flutterlib/ui/widgets.dart (OrderStatusPanel), lib/ui/screens.dart (OrderStatusScreen wrapping UiStateView<Order>). The switch on order.status returns a (label, color, message) record.

React Nativesrc/ui/widgets.tsx (OrderStatusPanel), src/ui/screens.tsx (OrderStatusScreen with UiStateView). A map[order.status] object gives the label, colour and message.

In all three, the panel takes an Order and an onDone callback — no fetching, no polling, no knowledge of where the order came from. That is what makes it previewable (see the previews recipe) and trivially testable.

Comparison

The taxonomy lands almost identically because Compose, Flutter and React all reward the same move: small stateless components, state in, events out. The only real difference is how each expresses the status→(label, colour, message) mapping — a Kotlin when returning a Triple, a Dart switch returning a record, a TypeScript lookup object — and all three are exhaustive over the four order states.

How to see it work

The Order Status panel appears in each app's Gallery in its paid, failed and pending states, and live at the end of the flow as polling settles the order. Because the panel is a pure function of an Order, the gallery renders all three states with three hard-coded orders.

Trade-offs

More components means more files and more indirection than one big screen widget. The return is that the organism is reused by the gallery and the tests unchanged, the atoms are shared across all seven screens, and "change the paid colour" is a one-line edit in one atom rather than a search across screens.