Every Composable deserves a ViewModel
Solving the Android challenge of scoping a ViewModel per Composable with remember
Hallelujah, I thought! We finally have a declarative UI framework, Compose, to solve an endless series of dangers and complexities associated with building UIs in Android.
One of the biggest challenges we at Q42 and most developers face when building Android apps (especially when using cutting edge architectures like Server Driven UI), is making custom and really reusable components. The first part is a piece of cake with Compose 🎉, the second part... that is the difficult one. That is what this article explores: building really reusable components.
Those heavy ViewModels
In the HEMA app, we have built a fast and snappy user experience by embedding the locally cached data for some critical components, like Favorites, in the data tree of UI components coming from the Server Driven UI architecture.
Unfortunately, the good old Android View system does not support ViewModels per Views. Thus, we need to do verbose data combinations in a large ViewModel to generate the state of the screen. If only there was a way to avoid this work…
With the power of Compose to build UI trees and a little bit of imagination let's see how far we can get.
My favorite mistake
The Favorite ❤️ button is a great example of a reusable component. On one side we have the UI with favorited ❤️ and unfavorited 🤍 state icons, plus the animations between the two states. On the other side, we have the business logic to expose the latest favorited state to the UI side and also to handle all the favorited or unfavorited user events, hiding away all the complexity involved in these events.
Unfortunately, when we integrate these nicely decoupled pieces of code into one screen using Jetpack libraries we usually end up with something like this:
The Favorite Composable can be dropped into the screen easily. But to connect it with our FavoriteUseCase we need to wire the state and event listeners through a state holder, the screen's ViewModel.
This scales poorly with the complexity of the screen because more components means more business logic added to our screen's ViewModel. Besides, we will also need to wire everything up again to use it on a different screen 😪. Furthermore, this does not allow us to add or remove components from the screen at runtime, leading to either an inflexible UX or a poor app architecture.
This means, in simple terms, that we are connecting UI Lego blocks with business logic Lego blocks using a large glue class, the screen's ViewModel 🗜.
A solution to remember
Ideally, we would like to have a FavoriteViewModel with the only responsibilities of passing user events to the FavoriteUseCase and holding the state that our Favorite Composable will observe, all while following the lifecycle of the Composable. This feature has already been requested multiple times but the options so far have been disappointing.
Let's explore our options in Compose outside of existing ViewModel providers:
Remember
The remember function will keep our object alive as long as the Composable is not disposed of. Unfortunately, there are a few cases* where our Composable will be disposed of and then added again, breaking the lifecycle parity with the remember function. 😢
✅ Pros
- Simple API
❌ Cons
- remember value will NOT survive a configuration change
- remember value will NOT survive when going into the backstack
- remember value will NOT survive a process death
RememberSaveable
RememberSaveable comes to save the day (sorry if the joke made you cry 😜), because this function will follow the lifecycle of the Composable, even in the few cases where the Composable is temporarily disposed of. But wait (there is always a but, and this one is a big one), the object we want to remember needs to implement Parcelable or the Saver interface in an additional class. 😢
This is a no-go for complex business logic objects. Furthermore, there are some transient states (like loading state of network calls) that you still want to keep between orientation changes but you don't necessarily want to survive after process death (an ongoing network call will definitely not survive app death) 😵💫. Hence, implementing these interfaces is not trivial.
✅ Pros
- rememberSaveable value will survive a configuration change
- rememberSaveable value will survive when going into the backstack
- rememberSaveable value will survive a process death
❌ Cons
- Complex integration work is required to correctly implement Parcelable or Saver
RememberScoped
Here comes ✨ rememberScoped 🎉, a tiny library I've built to support these use cases and complement the family of remember functions in Compose.
Objects stored by this function are kept in memory during the lifecycle of the Composable, even in a few cases* where the Composable is disposed of, and then added again.
✅ Pros
- Simple API
- rememberScoped value will survive a configuration change
- rememberScoped value will survive when going into the backstack
❌ Cons
- rememberScoped value will NOT survive a process death
With this tool in our toolbox, we can finally have reusable UI components (Composables) connected with their business logic (FavoriteViewModel & FavoriteUseCase) that are ready to drop into any screen without large glue classes (I'm looking at you, screen's ViewModel).
The example below puts rememberScoped to action. Notice how this Favorite composable could be easily applied to your project or the HEMA app with little adaptations, regardless of the architecture used for the business logic.
…then every Composable got their right to its own ViewModel, and the world became a better place. *barely legitimate quote
One more thing
This example is just the beginning. I can't wait to see what other cool things we will be able to build in Compose that we haven't even imagined yet. What I can already share is the big impact this will have on our most beloved app architecture at Q42: Server Driven UI.
In the case of the HEMA app, we won’t need to do those verbose UI tree data combinations anymore and this will be the first step towards a much simpler codebase. But that is another story for another time.
We have come a long way from the days of the God Activity, let's keep pushing forward together 💪
Note
You might assume this article is screaming “death to the screen’s ViewModel”, but far from that. I believe no solution is a silver bullet, and the scoping technique presented in this article should not replace all existing ViewModels because most screens will still need a general state holder. This library gives us one more option to pick from when selecting the right solution for the right problem.
*Cases where a Composable is disposed of, and then later added again:
- Android configuration changes (e.g. screen rotation, split-screen, etc.).
- When a Composable container goes into the backstack and then back to the foreground. The Composable has to be hosted inside a destination from the Navigation Compose library or inside a Fragment.
- When the app is in the background and Android triggers a process death (the user does not request nor notice that the app has been killed).