Android

5 Jetpack Compose patterns I use in every app

2026-06-12
6 min read
Abhilash — MakeLabs

After a year of building production Android apps with Jetpack Compose, some patterns have become reflexive. These aren't clever tricks — they're the boring, solid foundations I reach for before writing a single composable.

1. Single source of truth UI state

Every screen gets one sealed class that represents every possible state. No scattered booleans. No nullable loading flags.

sealed class ShiftUiState { object Loading : ShiftUiState() data class Success(val shifts: List<Shift>) : ShiftUiState() data class Error(val message: String) : ShiftUiState() } // In the ViewModel val uiState: StateFlow<ShiftUiState> = _uiState.asStateFlow()

The composable then uses a when expression on the state. Every case is handled, the compiler tells you when you miss one, and the UI is always predictable.

2. Hoisted state, dumb composables

Composables shouldn't own state they don't need to. Push state up, pass values and lambdas down. This makes every composable independently testable and reusable.

// Don't do this @Composable fun ShiftCard() { var expanded by remember { mutableStateOf(false) } ... } // Do this @Composable fun ShiftCard( expanded: Boolean, onToggle: () -> Unit ) { ... }

3. A single Typography object

Define all text styles once. Never hardcode font sizes or weights inline. When I want to tweak the design, I change one file and every screen updates.

object AppType { val heading = TextStyle( fontFamily = InterTightFamily, fontWeight = FontWeight.Bold, fontSize = 20.sp, letterSpacing = (-0.5).sp ) val body = TextStyle( fontFamily = InterFamily, fontSize = 14.sp, letterSpacing = 0.1.sp ) }

4. Preview every state

Write a @Preview for loading, success, empty, and error. Not just the happy path. This takes five minutes and saves hours of running the emulator to check edge cases.

@Preview(name = "Loading") @Composable fun ShiftScreenLoadingPreview() { ShiftScreen(state = ShiftUiState.Loading) } @Preview(name = "Empty") @Composable fun ShiftScreenEmptyPreview() { ShiftScreen(state = ShiftUiState.Success(emptyList())) }

5. Explicit navigation graph

Define all routes as constants in one place. String literals scattered through a nav graph are a bug waiting to happen.

object Routes { const val HOME = "home" const val ADD_SHIFT = "add_shift" const val SETTINGS = "settings" const val SHIFT_DETAIL = "shift/{id}" fun shiftDetail(id: String) = "shift/$id" }
None of these patterns are original. They're straight from the official Compose guidance. The value isn't novelty — it's consistency. Pick your patterns early and apply them everywhere.

These five things are in every MakeLabs app from day one. They're not exciting, but they're the reason the codebase stays manageable when it's just one person — with AI assistance — maintaining it.

← Previous post
Building apps solo in 2026
Next post →
What launching on Google Play actually looks like