← Jetpack Compose from Zero to Production

Theming, previews and tests

Lesson 5 of 51 min read

One place for colour and type, previews that replace rebuilds, and the three tests worth writing.

This is the lesson that separates a tutorial app from a shipped one.

Theme in one place

@Composable
fun AppTheme(dark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = if (dark) DarkColors else LightColors,
        typography = AppTypography,
        content = content,
    )
}

Then never hard-code a colour in a screen. Use MaterialTheme.colorScheme.primary and MaterialTheme.typography.titleMedium. The payoff arrives the day someone asks for dark mode or a brand refresh: one file changes, not ninety.

Previews instead of rebuilds

@Preview(name = "Light")
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun ProductRowPreview() {
    AppTheme { ProductRow(sampleProduct) }
}

Preview every state, not just the happy one: loading, empty, error, long text, and a large font scale. A stateless composable makes this trivial — which is the practical reason state hoisting pays off.

Accessibility, the two-minute version

  • contentDescription on meaningful images; null on decorative ones.
  • Touch targets at least 48 dp.
  • Check at 200% font scale — fixed height on a text row is the usual casualty.

The three tests worth writing

@Test
fun shows_empty_state_when_there_are_no_items() {
    composeRule.setContent { AppTheme { FeedScreen(FeedUiState(items = emptyList())) } }
    composeRule.onNodeWithText("Nothing here yet").assertIsDisplayed()
}
  • ViewModel unit tests — the logic, on the JVM, fast.
  • A screenshot or state test per screen — catches layout regressions cheaply.
  • One or two journey tests — the flow that earns money.

The usual mistake: testing that a Text exists with the exact production copy. Marketing changes a word and your test fails for no reason. Assert on behaviour and test tags, not prose.