Compose (UI) beyond the UI (Part III): No AAC-ViewModel and a sample app

Jordi Saumell
ProAndroidDev
Published in
6 min readMar 2, 2021

--

In the previous articles I have proposed several significant changes to traditional Android development, and there was one last subject left to be explained: how do we avoid using the Android ViewModel. So in Part III we will focus on this, and then I will present an open source sample app that I built to showcase what we have seen in this series, and a bit more.

To better understand the reasons for what we are doing, it is advisable to read Part I and Part II.

Let’s first recall why we want to avoid the Android ViewModel in apps that only use Compose for the UI. First, to avoid having memory leaks due to the ViewController living longer than its view, especially with unhandled configuration changes. Second, to be able to instantiate our ViewControllers, without requiring factories. Third, because it was built to survive configuration changes, and we have seen that we can avoid configuration changes. And finally, so that it is platform agnostic.

Replacing the Android ViewModel

What do we need to build our own ViewController?

  • A way to retrieve the same instance while its lifecycle is alive, or in other words, while the user has not left the screen or view.
  • A way to cancel it when its lifecycle ends, to cancel ongoing tasks like coroutines.
  • A way to save user state.

Scoping a ViewModel to its lifecycle

There are many ways to handle scopes or lifecycles for object instances. I use Koin, but you can use manual injection (a simple map, for instance) or another DI or service locator framework.

Once we are set with a tool, we want to bind the scope to the view that it is associated with. Typically a screen or a flow of screens (a navigation sub-graph). We will need an id or key to distinguish that scope from others. Here what I do is build it with the path used for navigation. This includes the value for each parameter, as we could have several instances of the same screen with different parameters.

In the case of Koin I use getOrCreateScope to retrieve the scope if it already exists, or create it otherwise. And from the scope we can obtain the ViewController. The ViewController (and other classes that need to be notified when the scope is cancelled) implements a Clearable interface, and before cancelling the scope all clearables are retrieved and cleared.

Cancelling the scope

To cancel the scope we need to register a listener that is called when the screen is dismissed. I first tried using the onCommit effect (you can read a very useful guide on effect handlers here) with the NavBackStackEntry, but I found that it was being called again when reentering the screen. This registered multiple callbacks. After discussing it on the compose slack I opened an issue and ended with a solution that is hacky but works: using rememberSavedInstanceState (now rememberSaveable) which is indeed only called once.
With this we only have a way to add a listener, but what listener? The NavBackStackEntry of the screen has an associated lifecycle, and we can attach a LifecycleEventObserver to it. So we observe its lifecycle events and in ON_DESTROY we close the scope, with Koin or the equivalent in your tool of choice.
This solution is a bit hacky, and it relies on the lifecycle of the navigation component. For these reasons, I opened a feature request for an effect handler that is only called once when a composable is added to be saved, and once when it is permanently removed. In case you are interested in this feature request, you can find it here.

Saving state

For ViewControllers that are tied to a screen or sub-graph we nave an associated NavBackStackEntry. We can get the SavedStateHandle from there when building the navigation and provide it to the ViewController through its constructor.
For classes that are not related to a navEntry I did not find a way to obtain the SavedStateHandle. So, in these cases I use the API provided by Compose to save state: rememberSaveable. In the sample app there is an example, and an abstraction over the saved handle to simplify all of this.

The sample app: Composing Clocks

Now that we have covered all the topics we are ready to start building apps with Compose. To help with that, and demonstrate the different topics explained in this series, I have published an open source sample. Here I will introduce some of the highlights, and the rest can be found in the readme and in the source code!

It is an app about clocks and timezones, and it is not a reference in UX. It only has the minimum number of screens to show all what was intended, and uses mock data to avoid wasting time on API calls and API credentials.

No usage of AppCompatActivity, Fragment or AAC ViewModel

This is the main topic in this series of articles so, of course, AAC ViewModels and fragments are not used. Regarding the Activity, AppCompatActivity is not used simply to reduce the size.

Language and orientation changes

Common configuration changes are handled, so we avoid recreating the Activity. Compose handles updating the UI and texts automatically.

As overwriting the system language is somehow common, this functionality is also provided. So, in the Configuration screen of the app there is a toggle to select the language, which updates the app without recreating the Activity or flashing the UI. It is an improvement on user experience, and although it will not be appreciated too often, it does not require extra development effort.

How to force the language change is explained in the readme, as it needs some considerations.

Adapting layout to window size and orientation

Apps generally do not support landscape or multi-window, among other reasons, because with the old UI toolkit it is costly. With Compose it is a matter of having an if only on what needs to change, and the rest remains as is. Hopefully we will see an improvement on UX in both of these situations. Here you can see a simple example of adapting the UI to both of these situations.

ViewModels for a screen, for a flow, and for the whole app

One of the core topics has been substituting Android ViewModels with our own ViewControllers. In the sample app we can see several examples of ViewModels that support a screen, a ViewModel that is attached to a flow of screens (a navigation sub-graph), and an app-wide ViewModel. It is achieved with the help of Koin scopes, and custom-made utilities to bind scopes to navigation.

Saving state

As saving the user state is crucial for a good user experience and it has been another important topic of this series, the app shows how to save state for all ViewModels (screen, flow, and app). It also saves the state of the mock usecase that contains the cities the user adds.

Some utilities are provided to simplify saving state in conjunction with flow. Also, the state saver is provided with a wrapper, which makes it platform agnostic and allows having a different implementation for when we do not have a SavedStateHandle.

Theming

Theming is barely touched, but the user is allowed to theme the clock to show that it is very easy to do with Compose. Also, the clock code shows how easy it is to build a custom view that can be customized and that has some basic animations.

Navigation

The navigation component is used for navigation, with utilities that bind destinations to Koin scopes and help with building the routes. Also, navigation setup works well with modularisation by feature, as modules can be completely decoupled. Each module can provide a route for each screen and require lambdas to inform to the root module that it is ready to navigate, and pass relevant parameters.

Bottom bar navigation

The app also shows how to set up navigation with the bottom bar. A feature that is not in the official documentation is that the selected tab is retained when the user navigates to a subscreen of a tab.

You can see more in the readme and source code!

Wrap up

This is the end of the journey. With these three articles we have discussed some improvements that Jetpack Compose brings to Android development. We have also seen how we can bring even more changes to how we build our apps, and with that simplify the development, so that we can focus on features and UX. Finally, I shared a sample that provides a way of solving several of the problems we encounter when building apps, while applying the changes proposed in this series.

I hope you enjoyed this process as much as I did, and please, share your thoughts and propose improvements or alternatives!

--

--

Self-learner Android passionate. Android tech-lead in progress at Basetis