Intro
To celebrate Jetpack Compose finally hitting 1.0, I wanted to take a bit of a deep dive to see what the framework has to offer. Instead of more of the same code labs with useful UI, I decided to bring back an old reddit trend of creating the absolute worst volume control UI possible (highlights here). You can take a look at the code on my Github.
The Results
Microtransactions | Radio Buttons | Catapult (WIP) |
---|---|---|
Digits Of PI | Seven Segment |
---|---|
Takeaways
As someone who’s never really been a fan of UI work, building UI in Compose has been extremely pleasant and straightforward. Things like custom animations that I haven't done in the View system were surprisingly simple and concise. With Compose I no longer need to context switch between writing Kotlin code and XML layouts, the flow between writing backend code and UI code is much more seamless. The best part was the often lauded LazyColumn
in compose that turns 3 files worth of boilerplate RecyclerView
code into a couple lines. While it took some effort to change my approach for the Compose mental model, this is a huge leap in the right direction towards making UI work quicker for experienced devs and easier for new devs to learn.
Lessons Learned
A vital concept when learning Compose is how the runtime manages state and recomposition via (Mutable)State
objects. Most of the "magic" of recomposition relies on the getValue
and setValue
methods of these classes. and if you mistakenly go around these methods to read and update your apps state, you can actually prevent the UI from updating as you intended.
I learned this the hard way while implementing the editable seven segment display by using a MutableState<MutableList>
and updating the underlying MutableList
values on user interaction (i.e. state.value[index] = newValue
). Desugaring that statement to state.getValue()[index] = newValue
we see that we're never calling MutableState.setValue
, so the Compose runtime has no indication of the underlying value change, never triggering a recomposition. On top of that, even if something else triggered a recomposition, the UI wouldn't be updated since we mutated the list without changing the reference, so the underlying equality check used to determine if our composable should run again never detects a change.
Luckily,Compose gives us a mutableStateListOf(...)
and List.toMutableStateList()
that return a SnapshotStateList
, which is a subclass of MutableList
(and equivalents for Maps
). That means we can just treat it as a MutableList
and the state updates will be handled for us when we do things like snapshotStateList[index] = newValue
. The main rule of thumb to learn from this is that in Compose you should avoid mutating objects you intend to use as a state and instead emit a brand new value to your LiveData/Flow/State using the copy
method on data classes. You can force this practice on yourself by avoiding var
s entirely in classes used for UI state.
For a demonstration of this issue, here’s a snippet showing my initial MutableList
approach, a functional workaround I made using a list of mutable states, and the proper solution using mutableStateListOf()
.
Conclusion
Now that Compose is officially at 1.0, you should give it a shot ASAP. Wherever you update the state of your UI, keep an eye out for spots that mutate the value in the State
instead of the State
itself. Above all, don't be afraid to get creative and have fun when playing with compose, you might be surprised by the things you're able to make easily compared to the old View system.
Top comments (0)