Alex Gabor

A quick intro to Jetpack Compose

21 June 2022

This document shortly introduces the “primitives” of Compose. For a more detailed guide see the official documentation.

@Composable and recompositions

The Composable function is the building block for working with Compose. Compose tracks your Composables and injects code into them that achieves state management and recomposition. So when building UI in Compose you only define its structure, but Compose decides when to call your Composables.

// Structure:
@Composable
fun LoginScreen() {
    Column {
        UsernameField()
        PasswordField()
    }
}

Your composable is called when it first enters the composition (when it first appears on the screen) and it will also be called whenever the state it depends on changes, known as recomposition.

The important takeaway out of recomposition is that you shouldn’t make assumptions about when your functions are called. In the example above PasswordField() may be recomposed and UsernameField() skipped.

Instead, Compose provides hooks into the lifecycle of a Composable called effects.

remember()

Since your composable is called any number of times, you will often need to cache values across recomposition.

This is what remember achieves:

val myStateHolder = remember { MyStateHolder() }

Its block will only be called when it first enters the composition. On recomposition it will return the same value.

You may invalidate the cached value by providing one or more key arguments:

val myStateHolder = remember(id) { MyStateHolder(id) }

This means that on recomposition only if the id changes it will call again the block.

mutableStateOf()

You’ll often create a mutableStateOf in the block of your remember:

var loading by remember { mutableStateOf(false) }

if (loading) {
    LoadingIndicator()
} else {
    Button(onClick = { loading = true }) {
        Text("Click me!")
    }
}

mutableStateOf creates a state that is tracked by compose, meaning that assigning a new value to it triggers a recomposition.

This means that it is actually an observable value similar to LiveData when used in data binding.

Whenever you’re reading its value you’re actually observing the value, but without having to call observe.

Since we’re comparing state with LiveData, a useful mention is that the equivalent of Transformations.map(dependantLiveData) { } is derivedStateOf:

var email: String by remember { mutableStateOf("") }
var password: String by remember { mutableStateOf("") }

val loginEnabled by remember { derivedStateOf { email.isNotEmpty() && password.isNotEmpty() } }

Notice how it automatically depends on captured state instead of having to specify on what it depends.

Basic layouts

Here are the common layouts and their View equivalents:

// LazyColumn example:
LazyColumn {
    items(myList, key = { it.id }) {
        Text("${it.someField}")
    }
}

Modifiers

When you need to alter how a composable looks or behaves, you will often use a Modifier.

All available layouts accept modifiers and it is a good practice to have a modifier parameter when you’re defining your custom layouts.

@Composable
fun HomeScreen(
    modifier: Modifier = Modifier,
) {
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .systemBarsPadding()
            .background(AppTheme.colors.surface)
    ) {
    // ...
    }
}

Keep in mind that the order of modifiers matter:

Box(
    modifier = Modifier
        .padding(16.dp)
        .clickable { /* ... */ }
        .padding(16.dp)
) {
    // ...
}

In the example above the clickable area doesn’t include the first padding, but includes the second padding.

CompositionLocal

Locals are tree-scoped values that can be accessed at any time in a composable.

Let’s explain what that means. The UI is a tree-like structure in which our composables are the nodes1.

At any time in that tree we can access a local like this:

@Composable
fun Foo() {
    val context = LocalContext.current
}

A CompositionLocal is tree-scoped because the value it contains may be different based on the location in the UI tree. When assigning a value to a local (using CompositionLocalProvider) its value is changed only in the subtree of that node.

The best use case for this is theming. You could achieve a theme overlay by providing different values like this:

val lightColors = LocalColors.current // here it returns the old color values
CompositionLocalProvider(LocalColors provides darkColors) {
    // new color values are returned in the subtree starting from this node
    val darkColorsHere = LocalColors.current
}
val stillLightColors = LocalColors.current // here it still returns the old color values

You can create your own Local using staticCompositionLocalOf like this:

    val LocalColors = staticCompositionLocalOf { lightColors }

Side effects

Side effects are hooks into the lifecycle of a composable. In short:

For a far better explanation see the official documentation.

rememberSaveable()

While remember saves values across recompositions, rememberSaveable saves values also across configuration changes and process recreation. The requirement is that the values need to be serializable into a bundle.

For types that can’t be saved automatically into a Bundle, you can either make the type parcelable, or you could create a Saver:

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Read more about rememberSaveable in the official docs.


Thanks to Gergely Hegedüs for reviewing.


  1. Technically not accurate, but for the sake of simplicity, we can consider the composables as the nodes. The actual nodes are LayoutNodes, but you don’t necessarily need this detail when working in Compose day to day. If you’re interested though, you can check out this article that dives into the internals: https://jorgecastillo.dev/diving-into-mosaic