When to load data in ViewModels

Recently I had a surprisingly long discussion on an ostensibly easy question. Where in our code should we actually trigger the loading of our ViewModel’s data. There are many possible options, but let’s look at a few of them.

Josef Raska
ProAndroidDev

--

More than two years ago, Architecture Components were introduced to the Android world, in order to improve the way we develop our apps. A core part of these components is the ViewModel with LiveData, which is an observable lifecycle-aware data holder to connect an Activity with a ViewModel. ViewModels output data and Activities consume it.

This part is clear and does not result in too many discussions, however the ViewModel has to load, subscribe or trigger the load of its data at some point. The question remains as to when this should be done.

Pic by Xevi Camacho with ❤

Our Use Case

For our discussion, let’s use a simple use case of loading a contacts list in our ViewModel and publishing it using LiveData.

What do we want

To have some criteria to evaluate, let’s first set the requirements we have for an effective loading technique:

  1. Take advantage of ViewModel to load only when needed, decoupling from lifecycle rotation and configuration changes.
  2. Easy to understand and implement, using clean code.
  3. Small API to reduce the knowledge required to use the ViewModel.
  4. Possibility to provide parameters. ViewModel very often needs to accept parameters to load its data.

❌ Bad: Calling a method

This is widely used concept and is promoted even in Google Blueprints example, but has significant problems. The method needs to be called from somewhere and this typically ends up in some lifecycle method of Activity or Fragment.

➖We reload on each rotation, losing the benefit of decoupling from the Activity/Fragment lifecycle, as they have to call the method from onCreate() or another lifecycle method.
➕Easy to implement and understand.
➖One more method to trigger.
➖Introduces implicit condition that the parameters are always the same for the same instance. loadContacts() and contacts() methods are coupled.
➕Easy to provide parameters.

❌ Bad: Start in ViewModel constructor

We can easily ensure data is only loaded once by triggering the loading within the constructor of the ViewModel. This approach is also shown in docs.

➕We load data only once.
➕Easy to implement.
➕Whole public API is one method contacts()
➖Not possible to provide parameters to load function.
➖We do work in constructor.

✔️ Better: Lazy field

We can use the lazy delegated property feature of Kotlin like:

➕We load data only when we first access the LiveData.
➕Easy to implement.
➕Whole public API is one method contacts()
➖Not possible to provide parameters to load function other than adding a state, which has to be set before contactsLiveData field is accessed.

✔️ Good: Lazy Map

We could use lazy Map or a similar lazy init based on the parameters provided. When the parameters are Strings or other immutable classes, it is easy to use them as keys of a Map to get LiveData corresponding to the provided parameters.

➕We load data only when we first access the LiveData.
➕Moderately easy to implement and understand.
➕Whole public API is one method contacts()
➕We can provide parameters and the ViewModel can even handle multiple parameters at the same time.
➖Still keeps some mutable state in the ViewModel.

✔️ Good: Library method — Lazy onActive() case

When using Room or RxJava, they have adapters to be able to create LiveData directly in @Dao objects, respectively using extension method on Publisher.toLiveData()

Both library implementations ComputableLiveData and PublisherLiveData are lazy in the sense that they do the work when LiveData.onActive() method is called.

➕We load data lazily only when lifecycle is in active state.
➖Loading is still coupled to lifecycle, because LiveData.onActive() means basically (onStart() and have observers).
➕Easy to implement and uses support library.
➕Whole public API is one method contacts()
➖In this example we create new LiveData per method call, to avoid this we would have to solve the problem of possibly different parameters. Lazy Map can be of help here. Example here.

✔️ Good: Pass the parameters in constructor

In the previous case with the lazy Map option, we used the Map only to be able to pass parameters, but in many cases one instance of ViewModel will always have the same parameters.

It would be much better to have the parameter passed to the constructor and use lazy load or start load in constructor. We can use ViewModelProvider.Factory to achieve this, but it will have some problems.

➕We load data only once.
➖Not trivial to implement and understand, boilerplate needed.
➕Whole public API is one method contacts()
➕ViewModel accepts parameters in constructor, immutable and nicely testable.

It requires extra code to hook into the ViewModelFactory in a way that we could pass dynamic parameters. At the same time we start to have problems with other dependencies and we need to figure out how to actually pass them into the factory together with parameters, creating even more boilerplate.

Assisted Injection is trying to solve this problem and Jake Wharton covered this topic in his talk at Droidcon London 2018. However there is still some boilerplate left, therefore even if this could be “perfect” solution, other options might be more suitable for your team.

Which approach to choose

Introducing Architecture Components significantly simplified Android development and solved many problems. Nevertheless there are still some questions left and we discussed here the problem of loading ViewModel data and evaluating various options.

From my experience, I recommend the Lazy Map approach as I found it to be a nice balance of pros and cons and is really easy to adopt. You can find examples here or here.

As you can see, there is no perfect solution and it is up to your team to pick the approach that fits you best, balancing robustness, simplicity and consistency across your project. Hopefully this post will help you choose.

Happy coding!

--

--

Key to being a good engineer is largely to use your judgment and avoid problems that would require a good engineer to solve them.