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.
|Microtransactions||Radio Buttons||Catapult (WIP)|
|Digits Of PI||Seven Segment|
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.
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
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
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
vars 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
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.