Jose Alcerreca

ViewModels and LiveData: Patterns + AntiPatterns

Translator: Bingxin said

The View and ViewModel

Assign responsibility

Ideally, the ViewModel should know nothing about the Android world. This improves testability, memory leak safety, and modularity. The usual practice is to ensure that your ViewModel does not import any Android.*, except Android.arch.*. The same goes for Presenter(MVP).

❌ Keep viewModels and Presenters away from classes in the Android framework

Conditional statements, loops, and general logic should be executed in the ViewModel or other layer of the application, not in activities and fragments. Views are generally not unit tested unless you’re using Robolectric, so the less code there is, the better. The View just needs to know how to present the data and send user events to the ViewModel/Presenter. This is called Passive View mode.

✅ Keep the logic in your Activity/Fragment as simple as possible

View reference in ViewModel

ViewModel and Activity/Fragment have different scopes. When the Viewmodel is alive and running, the activity can be in any state of the lifecycle state. Activitie and Fragment can be destroyed and recreated without the ViewModel being aware of it.

Passing a reference to a View(Activity/Fragment) to a ViewModel is a big risk. Suppose the ViewModel requests the network and returns the data later. If the View reference has been destroyed, or the Activity has become invisible. This can lead to memory leaks and even crashes.

❌ Avoid holding a reference to the View in the ViewModel

The recommended way to communicate between viewModels and views is in observer mode, using LiveData or other class library observables.

Observer model

A very convenient way to design a presentation layer in Android is to have a View observe and subscribe to the ViewModel. Since the ViewModel doesn’t know anything about Android, it doesn’t know how frequently Android kills views. This has the following benefits:

  1. The ViewModel remains constant through configuration changes, so there is no need to re-request resources (database or network) when the device rotates.
  2. When the time-consuming task completes, the observable data in the ViewModel is updated. It doesn’t matter whether the data was observed or not, trying to update a View that doesn’t exist does not cause a null pointer exception.
  3. The ViewModel does not hold a reference to the View, reducing the risk of memory leaks.
private void subscribeToModel(a) {
  // Observe product data
  viewModel.getObservableProduct().observe(this.new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) { mTitle.setText(product.title); }}); }Copy the code

✅ lets the UI observe changes to the data, rather than pushing data to the UI

Fat ViewModel

No matter what makes you choose layering, it’s always a good idea. If your ViewModel has too much code and too much responsibility, then:

  • Remove some of the logic to a place with the same scope as the ViewModel. This part will communicate with the rest of the application and update the LiveData held by the ViewModel.
  • Using Clean Architecture, add a Domain layer. This is a testable, maintainable architecture. Architecture Blueprints has examples of Clean Architecture.

✅ distribute responsibility and, if necessary, add domain layers

Using a data warehouse

As described in the Application Architecture guide, most apps have multiple data sources:

  1. Remote: Network or cloud
  2. Local: database or file
  3. Memory cache

It is a good idea to have a data layer in your application that is completely separate from your view layer. The algorithm for keeping caches and databases in sync with the network is not simple. A separate Repository class is recommended as a single entry point for handling this complexity.

If you have multiple data models, consider using multiple repositories.

✅ Add a data warehouse as a single entry point for your data.

Processing data state

Consider the following scenario: You are looking ata LiveData exposed by the ViewModel that contains list items that need to be displayed. So how does a View distinguish between loaded data, network error, and empty collection?

  • You can expose a LiveData

    from the ViewModel. MyDataState can contain information about data being loaded, loading completed, error occurred, etc.

  • You can wrap data in classes that have status and other metadata, such as error messages. Look at the Resource class in the example.

✅ uses a wrapper class or another LiveData to expose the state information of the data

Save the Activity state

The information needed to recreate the screen when an activity is destroyed or the process is killed and the activity is not visible is called the activity state. Screen rotation is the most obvious example, and if the state is stored in the ViewModel, it is safe.

However, you might need to restore state if the ViewModel doesn’t exist either, such as when the operating system kills your process due to resource constraints.

To effectively save and restore UI state, use a combination of onSaveInstanceState() and ViewModel.

See: ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders.

Event

Event Indicates an Event that occurs only once. The ViewModel exposes data, but what about events? For example, navigation events or displaying Snackbar messages are actions that should only be performed once.

LiveData stores and restores data, which is not exactly the same concept as an Event. Look at a ViewModel with the following fields:

LiveData<String> snackbarMessage = new MutableLiveData<>();
Copy the code

The Activity starts observing it and updates its value when the ViewModel finishes an operation:

snackbarMessage.setValue("Item saved!");
Copy the code

The Activity receives the value and displays the SnackBar. That’s obviously how it should be.

However, if the user rotates the phone, a new Activity is created and observed. When the LiveData observation begins, the new Activity will immediately receive the old value, causing the message to be displayed again.

Rather than using libraries or extensions of architectural components to solve this problem, think of it as a design problem. We recommend that you treat events as part of the state.

Design events as part of the state. For more details please read LiveData with SnackBar,Navigation and Other Events (The SingleLiveEvent Case)

The leakage of the ViewModel

Responsive programming works well in Android thanks to the easy connection between the UI layer and the rest of the application. LiveData is a key component of this pattern, and both your activities and fragments observe LiveData instances.

How LiveData communicates with other components is up to you, be aware of memory leaks and boundary conditions. As shown in the figure below, the Presentation Layer uses observer mode and the Data Layer uses callbacks.

When the user exits the application, the View is no longer visible, so the ViewModel does not need to be observed. If a data warehouse Repository is a singleton and scoped with the application, it will not be destroyed until the application process is killed. This can only happen if the system is running out of resources or if the user manually kills the application. If the data warehouse Repository holds a reference to the ViewModel’s callback, the ViewModel will leak memory.

If the ViewModel is lightweight, or if the operation is guaranteed to end quickly, such leaks are not a big deal. But that’s not always the case. Ideally, the ViewModel should be released as long as it is not being observed by the View.

There are several ways you can do this:

  • Notifies the data warehouse to release the ViewModel callback via viewModel.oncleared ()
  • Use weak references in a data warehouse Repository, or Event Bu (both prone to misuse and even considered harmful).
  • Process communication between the data warehouse and the ViewModel is performed by using LiveData in the View and ViewModel

✅ consider how boundary cases, memory leaks, and time-consuming tasks can affect instances in the architecture.

❌ Do not store state or data-related core logic in the ViewModel. Every invocation in the ViewModel may be the last operation.

LiveData in the data warehouse

To avoid ViewModel leaks and callback hell, the data warehouse should be observed as follows:

When the ViewModel is cleared, or the View’s life cycle ends, the subscription is also cleared:

There is a problem if you try this approach: what if you subscribe to the data warehouse via ViewModel without accessing the LifeCycleOwner object? You can do this very easily with a quick quick doubling. Transformations. SwitchMap according to the variation of a LiveData instance allows you to create new LiveData. It also allows you to pass observer lifecycle information via the call chain:

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
        if (repoId.isEmpty()) {
            return AbsentLiveData.create();
        }
        returnrepository.loadRepo(repoId); });Copy the code

In this example, when an update is triggered, the function is called and the results are distributed downstream. If an Activity observes the repo, the same LifecycleOwner will be applied to the call to repository.loadrepo (repoId).

Transformation is a good solution whenever you need a LifeCycle object inside the ViewModel.

Inheritance LiveData

The most common way to use LiveData in a ViewModel is MutableLiveData, and to expose it externally as LiveData to ensure immutable to the observer.

If you need more functionality, inheriting LiveData will let you know about active observers. This is useful when you listen for location or sensor services.

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // Initialize service
    }

    @Override
    protected void onActive(a) {
        // Start listening
    }

    @Override
    protected void onInactive(a) {
        // Stop listening}}Copy the code

When not to inherit LiveData

You can also enable the service to load data by onActive(). But unless you have a good reason why you don’t need to wait for LiveData to be observed. Here are some common design patterns:

  • toViewModeladdstart()Method and call it as soon as possible. [seeBlueprints example]
  • Set a property that triggers loading [see GithubBrowerExample]

You don’t need to inherit LiveData very often. Let the Activity and Fragment tell the ViewModel when to start loading data.

The divider

Translation here, in fact, this article has been lying in my favorites for a long time. Google has recently rewritten the Plaid application, using a range of the latest stacks, AAC, MVVM, Kotlin, coroutines, and more. This is one of my favorite technology stacks, and I opened the Wanandroid app based on it. Kotlin+MVVM+LiveData+ Coroutine build Wanandroid! .

At that time, BASED on the superficial understanding of MVVM, I wrote a set of MVVM architecture that I thought was MVVM. After reading some articles about architecture and Plaid source code, I found some cognitive misunderstandings of my OWN MVVM. In the future, the Wanandroid application will be reasonably reformed, and some explanations will be made based on the knowledge points mentioned in the above translation. Welcome to Star!

This article first published wechat official account: BingxinshuaiTM, focusing on Java, Android original knowledge sharing, LeetCode problem solving.

More latest original articles, scan code to pay attention to me!