Jetpack Compose is Google’s newest UI framework to build Android (and desktop) apps. It greatly shifts the approach used in building UI components. It is fully declarative, meaning you describe your UI by calling a series of functions that transform data into a UI hierarchy. It’s unbundled meaning it is backwards compatible with older versions of Android and can be updated separately from the Android OS.
But most importantly, it makes writing UI code very easy. How easy, you ask? As easy as the following code:
This short function creates a list in the UI that uses RecyclerView internally.
And you can build any UI in this LazyColumn, imagine developing it by yourself by adding an adapter, ViewHolder, DiffUtil class, and a RecyclerView in the layout.
In this article, we would like to share the most interesting facts we have come across while learning Compose.
1. Compose is interoperable with Android Views
You can use Compose in Android Views and Android Views in Compose. This is great because Compose will work with your existing UI implementation. If you want to add it to your project, you don’t have to go all the way and do it in one go. You can start slow, introduce the basics like theming, styles, and gradually integrate Compose in your codebases.
2. A composable function might get executed within a pool of background threads
Unlike Android Views, Compose can run your composable functions (where you declare your UI) in background threads! It can optimize recomposition (updating the parts of UI where changes occurred) by running composable functions in parallel. This let us Compose take advantage of multiple cores, and run composable functions not on the screen at a lower priority.
3. All composable functions and lambdas must be side-effect-free
A composable function must have these attributes:
- The function behaves the same way when called multiple times with the same argument, and it does not use other values such as global variables or calls to random().
- The function describes the UI without any side effects, such as modifying properties or global variables.
In the declarative programming paradigm that Compose uses, when you want to update your UI, you need to call the composable function again with new data. Doing this causes the UI to be recomposed — the widgets emitted by the function are redrawn, if necessary, with new data.
Recomposing your entire UI tree can be computationally expensive. Compose solves this problem with this intelligent recomposition. Recomposition is the process of calling your composable functions again when inputs change. When Compose recomposes based on new inputs, it only calls the functions or lambdas that might have changed and skips the rest. By skipping all functions that don’t have changed parameters, Compose can recompose efficiently.
When you need to perform a side effect, trigger it from a callback such as onClick instead of accessing local or global variables from the composable function. If you must keep a state in a compose function, you should use mutableStateOf and remember to not have a different state for every recomposition. This brings us to our next topic.
4. Compose doesn’t have an internal state
In Compose, state management is in the developer’s hands. By default, there is no internal state in Compose. This removes the sync issues between your app and Compose components.
For example, unlike Android Views, when you tap a checkbox or a toggle (it’s called Switch in Compose), it won’t toggle automatically by default in Compose. You need to code that “tapping on a toggle changes the toggle state” logic.
For example, consider the following Composable function:
This won’t make the checkbox automatically checkable. Because we still need to implement onCheckedChange callback and implement “clicking the checkbox makes it checked” logic.
You might think we can just add a Kotlin variable inside the Composable function like this:
However, this is not good practice, and it will not work. Because Compose doesn’t know this state, this value will be discarded in the next recomposition. We need to register this state in Compose so that it can consider it while doing recomposition.
Instead of a Kotlin variable, you should use the remember function:
Now with remember, Compose knows about our state. This way we can create a typical behaviour where you check and see that it is in the checked state.
There are other ways to store the state as well. Because you may not always want to store it in your Composable function. You can also take the necessary information and callback functions as parameters to your Composable function.
This allows the parent to control the child’s state and is called state hoisting. When building these functions you should think about whether the parent might be interested in the state, if yes then consider using this approach instead.