Introduction
Jetpack Compose is a modern, fully declarative UI toolkit created to address the challenges of the old View system.
Unlike the old View system, Compose provides a declarative API that enables your UI to react to changes, without imperatively mutating the frontend views.
Conversely, the old View system requires you to manually update the nodes (views) from the View hierarchy by calling methods such as textView.setText()
.
The View system approach increases the likelihood of errors since the actual state of the Views is internalized. That means you have two statesfor every UI component — the state you are maintaining in our application (e.g. data stored in a ViewModel), and the internal state of the View.
The challenge is to keep both states in sync
Declarative paradigm shift
The old View system builds the UI by instantiating a tree of Views. This is often done by inflating an XML layout file.
Each View maintains its own state and provides accessors and mutators that allow the application logic to interact with the internal state of the View.
However, in Compose, views are not exposed as objects.
Composable functions only describe the UI, and not construct it themselves.
So then, how does Jetpack Compose update its Views?
Whenever the state changes, the Composable functions are called again with the new data, and the UI is redrawn. This process is called Recomposition.
UI is a reflection of state
Recomposition
The main role of a Composable function is to convert the state into UI. It does so by calling (emitting) other Composable functions that create a tree-like hierarchy stored inside a SlotTable
.
SlotTable is a gap buffer implementation of the composition slot space. This implementation facilitates the Recomposition process because elements can be easily inserted and removed.
An element in the slot space can be a:
- Slot, which is the primitive base type of the slot space. It is of type
Any?
and can hold any value. - Group, which is a keyed group of Slots. It counts the number of slots and nodes it contains. Groups represent memoized function calls.
- Node, which is a special type of Group. Nodes represent Views.
The Compose framework can intelligently recompose only the components that changed.
Functions that do not depend on the changed state are not recomposed.
Composable “memory”
The state is considered to be anything that could influence the UI, at a point in time.
You can imagine how quickly the state can change in a dynamic application.
Animations change state at every frame.
Composable functions will recompose at every frame of the animation.
Thus, costly calculations would be computed at every frame.
To overcome this issue, complex calculations can be wrapped in a call to remember
.
The remember function works as follows:
- if the calculation was previously computed for the given inputs, then return it from the slot space
- otherwise, compute the calculation, store it in the slot space, and return it
If the list of readers doesn’t change, the value of awesomeReaders
will be computed only once, and it will be stored in the slot space.
Subsequent calls, during the Recomposition process, will query the slot space for the previously computed result of the calculation, and not do the calculation again.
If the list of readers changes (and hopefully it will for this article 😀), the calculation is performed again, and the result is stored in the slot space.
You can think of the remember
function as a map over the slot space. You use the inputs as the key to try to retrieve the corresponding value from the slot space.
But functions can forget…
Even though Compose exposes the remember mechanism, you shouldn’t always rely on it.
If a Composable function is completely removed from the slot space, you lose all data that the function “remembered”.
Values remembered in the slot space are forgotten when the calling composable is removed from the slot space.
This can easily happen in the case of LazyColumnFor
because cells are destroyed when they pass the scrolling limit, and the respective calculation is recomputed when they become visible again.
Truly memorable functions
Jetpack Compose provides the State type which acts as an observable state holder.
Composable functions can subscribe to this state and automatically recompose when the state changes.
You can declare a state object in the following ways:
It’s important to remember to remember
the State
object. Otherwise, it will be reinitialized with every Recomposition.
Whenever a Composable function reads the value of a state object, it automatically subscribes to the state.
But functions can still forget…
Even if your function remembers the state, how can you make sure that it won’t forget it?
Welcome back ViewModel!
The state can still be maintained by the ViewModel. The only difference is that you are ditching the LiveData for State.
This way, you can make sure that your state is preserved because the ViewModel is not tied to the lifecycle of a Composable function.
Compose provides delegates to ease working with State (see above example).
If you are unfamiliar with the delegation pattern in Kotlin, we invite you to read this article:
Don’t Reinvent the Wheel, Delegate It!Favouring delegation to inheritance with Kotlin native support |
![]() |
The State class works exclusively in the Compose context. It does not work in the old View system.
However, it turns out that LiveData can still be used with Compose.
You don’t even have to touch the ViewModel if you plan to progressively migrate your application to Compose.
All you have to do is use the observeAsState()
extension function to convert your LiveData to State inside Composable functions.
Conclusion
In the old View system, there are always two states — the application state and the View state.
The challenge is to keep them in sync.
However, Jetpack Compose natively supports the concept of State. The state is no longer internalized in the views and a single source of truth is exposed.
We have seen that Composable functions have ‘memory’ and can remember certain calculations… They can also forget them.
The most reliable place to hold the state is still the ViewModel.
If you progressively migrate your application to Compose, you might not even need to touch the ViewModel.
LiveData objects can be easily converted to State
with the use of built-in extension functions.
I hope you enjoyed this article.
If you found the information useful, don’t forget to clap.
Until next time! 😀
Reach out to me on LinkedIn if you have suggestions for future articles.
Denis Crăciunescu — Android Developer — iQuest Group | LinkedInView Denis Crăciunescu’s profile on LinkedIn, the world’s largest professional community. |
*illustrations generated using icons8 vector creator