Migration to Compose, from a ViewModel standpoint
Our trust grew more and more with Jetpack Compose, as we saw how it can improve UI implementation. It was faster, easier, and less error prone to develop our UI with it. So, a while after it became stable, we decided to migrate to Compose in our Spott app. But, like everything, it came with its own challenges. In this post, I’m gonna talk about some of the challenges that we had to overcome and how we did it.
Without further ado, let’s get to it:
Performance monitoring
If you’re just starting to migrate your application to compose, you should keep in mind that the UI performance visible in your app when your testing a debug version, is not the final performance of the app, and it’s more pronounced when you’re using compose.
Sometimes, lists might look laggy, bottom sheets might open late and other visual performance problems that might make you reconsider using views instead, but they all might only happen when you’re running the debug version of your app.
For more information, you can check here.
Combining ViewModels with StateHolders can be a life saver
At first, as we already had ViewModel
s in place, we wanted to store all UI states inside the ViewModel
. This meant that we had to add some new states that we didn’t need to add before migrating to compose. For example, we now had to store the currently selected radio button value, currently written text inside text fields, currently selected items, etc. as opposed to only sending them once the user clicks on the Apply button. This meant that we had to add a lot of new states to our current ViewModel
s, in addition to changing how the APIs for the ViewModel
s work. For example, we didn’t need to get any info from UI when user clicked on Apply button.
But after a while and facing different situations where we unnecessarily had to break down our UI state, we actually tried to implement this (example included) section of Compose state management, and in a lot of cases, it made our lives a lot easier by removing UI elements’ state from ViewModel
s.
Because state holders are compoundable and ViewModels and plain state holders have different responsibilities, it’s possible for a screen-level composable to have both a ViewModel that provides access to business logic AND a state holder that manages its UI logic and UI elements’ state. Since ViewModels have a longer lifespan than state holders, state holders can take ViewModels as a dependency if needed.
Handling one-off events
This issue, is not directly related to compose itself, but been made a lot more visible when migrating to compose. There’s a great blog post on this subject by Manuel Vivo which you can find here. But in a nutshell:
You should handle ViewModel events immediately, causing a UI state update. Trying to expose events as an object using other reactive solutions such as Channel or SharedFlow doesn’t guarantee the delivery and processing of the events.
Don’t forget about Combine
Though this is not directly related to compose itself, but is one of the things that helped us migrate to compose more easily.
For context, our ViewModel
s looked like this before migrating to compose:
As apparent, instead of a single UI state, we had multiple streams trying to publish the UI info. So, we wanted to reduce it to only one UI state which contained all related UI info. So first, we created the UI state data class
:
One way of doing it is using update
function of MutableStateFlow
and adding it wherever one of the UI info changes. Although pretty straight forward, but it requires you to change the ViewModel
and adds to the overall time of migration and review process.
Another way is to use combine
function to combine multiple StateFlow
s and create the UI state accordingly:
This way, with minimal changes to the ViewModel
, you can easily reduce your events and states into one UI state.
Extension properties can be powerful
Some UI state fields can be related to each other. For example, we wanted a button to be enabled only when there is a radio button selected and the screen is not in a loading state. Without extension properties, it can look something like this:
But using extension properties, it can look a lot better and easier to manage:
Now, you don’t need to explicitly change the state of button or worry about it carrying the wrong state.
SingleLiveData for events
Though already mentioned, view model events should be reduced to UI state, and then updated via UI events. But we found that sometimes, it’s not a good idea to go that route because of some API issues. For example, we wanted to display a Toast
, and understanding when Toast
is finished displaying can be a bit messy. I’m using Toast
as an example as it can happen for some other, specially older libraries and APIs.
If we wanted to implement it using UI states only, our code would probably looked like this:
Though it’s not a lot of work, but it makes you think about some corner cases like LifeCycle
as the API doesn’t support it. If we wanted to do this using SingleLiveEvent
in this case, the changes would’ve looked like this:
It’s a bit more minimal, and it works well enough for this use case. But be caution about using it, as the general practice for it is reducing the events to UI state, and it only should be done when the other options are not that straight forward.
Don’t forget rememberUpdatedState
One of the issues that I constantly ran into, was forgetting about using rememberUpdatedState
as everything worked well enough for me not to pay attention to it. But none the less, it should be there. If you’re interested in learning when to use it, you can check here.
Conclusion
Overall, migrating to Jetpack Compose was more straight forward than we thought. It helped us write cleaner UI related codes, develop faster, and manage our states easier. But it still has some of its own corner cases, which should be considered when migrating.
In the end, I want to mention that if you’re more curious about how to implement using Compose, or how to break down UI states, I strongly recommend that you take a look into Now in Android project which contains a lot of best practices and can play as a good sample project.