ViewModels and LiveData: Patterns + AntiPatterns

The official website has made some explanations on ViewModel, such as the inability to introduce Activity Context in ViewModel, but there are still many precautions, or idioms (common syntax) to better use ViewModel.

This article refers to a blog post recommended by Google's official website: ViewModels and LiveData: Patterns + AntiPatterns

Let's take a look at the overall description of the use of architectural components given on the official website:

Ideally, ViewModels should not introduce anything to Android. This improves testability, leak safety and modularity. The general rule of thumb is to ensure that there is no android. * Import (except android.arch. *) In the ViewModel. The same applies to the presenter layer.

❌Note 1: Don't introduce android framework class in ViewModel (or presenter layer)  

Conditional statements, loops, and general decisions should be done in ViewModel or other layers of the application, not in Activity or Fragment. View is usually not unit tested (unless you use Robolectric) so the fewer lines of code the better. The VIew view should only know how to display data and send user events to the ViewModel (or Presenter). This is called Passive  View passive view mode.

That is: Minimized logic code should be retained in Activity and fragment.

❌Note 2: In "ViewModel official website learning summary" article, it has been explained that because the life cycle of ViewModel is greater than Activity, it is not possible to introduce the context of Activity or fragment in the ViewModel layer to avoid memory leakage or crash.

✅: The interaction between ViewModels and View, the recommended way is the observer mode, such as using LiveData or observers in other libraries. That is, don't directly pass data to the UI layer, but let the UI layer observe the data changes, that is, the data drives the UI.

   

 

 Avoid bloated ViewModels

参考:Lifecycle Aware Data Loading with Architecture Components

✅: That is to avoid putting the data processing logic directly in the ViewModel, but in the LiveData (or other observable), because the purpose of the ViewModels design is to create and organize LiveData. Some logic can be transferred to the presenter layer.

✅: Encapsulate the logic in LiveData, it is also beneficial to reuse the same LiveData in many ViewModels, combine multiple LiveData sources through MediatorLiveData, or use them in a Service

See the following code:

public class JsonViewModel extends AndroidViewModel {
  // You probably have something more complicated
  // than just a String. Roll with me
  private final MutableLiveData<List<String>> data =
      new MutableLiveData<List<String>>();
  public JsonViewModel(Application application) {
    super(application);
    loadData();
  }
  public LiveData<List<String>> getData() {
    return data;
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void... voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        this.data.setValue(data);
      }
    }.execute();
  }
}

The above code puts a lot of data acquisition or processing logic into the ViewModel layer. This is unreasonable and should be changed to the following method:

public class JsonViewModel extends AndroidViewModel {
  private final JsonLiveData data;
  public JsonViewModel(Application application) {
    super(application);
    data = new JsonLiveData(application);
  }
  public LiveData<List<String>> getData() {
    return data;
  }
}
public class JsonLiveData extends LiveData<List<String>> {
  private final Context context;
  public JsonLiveData(Context context) {
    this.context = context;
    loadData();
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void… voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        setValue(data);
      }
    }.execute();
  }
}

Now our ViewModel has become quite simple. Our LiveData now completely encapsulates the loading process and only loads the data once.

 

Use the data repository layer

According to the description in the reference blog, in fact, the official website documentation also introduces that when the data may come from the network, or local memory, or cache, etc., you should add a repository layer to encapsulate these data processing operations, and then the presentation layer (presenter or ViewModels ) There is no need to care about where the data comes from. The repository layer is the unified entrance to the data.

✅: That is: add a data repository layer as the only entrance to external data.

 

Processing data status

Consider the following scenario: You are observing LiveData exposed by a ViewModel that contains a set of list data to be displayed. How does the view distinguish between loaded data, network errors and empty lists?
You can expose LiveData <MyDataState> from ViewModel. For example, MyDataState can contain information about whether the data is currently being loaded, whether it has been successfully loaded or failed, or some other raw data information.

✅: That is: use a wrapper class to expose the status information of your data, or use another LiveData.

 

Save the state of Activity

Activity status is the information needed to recreate the screen after the Activity disappears, indicating that the Activity has been destroyed or the process has been terminated. Rotation is the most obvious situation, we have covered it with ViewModels. If the state is saved in the ViewModel, the state is safe.
However, you may need to restore state in other scenarios where the ViewModel has also disappeared: for example, when operating system resources are insufficient and cause your process to terminate.
To effectively save and restore the UI state, use a combination of persistence, onSaveInstanceState () and ViewModels.
For examples, see: ViewModels: Persistence, onSaveInstanceState (), restore UI state and loader

 

Event

The event referred to here only occurs once. ViewModels expose data, but what about events? For example, navigation events, permission applications, or displaying Snackbar messages are actions that should only be performed once.
The concept of the event does not exactly match the way LiveData stores and restores data. Consider a ViewModel with the following fields:

LiveData<String> snackbarMessage = new MutableLiveData<>();

Activity starts to observe this operation, and ViewModel completes the operation, so the message needs to be updated:

snackbarMessage.setValue("Item saved!");

At this time, the Activity gets the value change and displays the Snackbar. It seems that there is no problem, but if the screen is rotated, a new activity is created and the old value will be received, which will cause the Snackbar to display the message again.

To solve this problem, you should not use third-party libraries or extend the architectural components, but should treat events as part of the state.

That is: design the event as part of the state. For more information refer to: LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) .

 

Pay attention to the leakage of ViewModel

Consider the following scenario:

The Presentation layer uses the observer mode, and the data layer uses interface callbacks.

If the user exits the app, the View disappears, and the ViewModel is no longer observed. If the repository layer is a singleton or the same as the application life cycle, the repository will not be destroyed until the process is killed. The process will only end when the system resources are tight or the user manually kills it. If there is a ViewModel callback in the repository layer, the ViewModel will temporarily leak memory.

 

If the ViewModel is relatively lightweight or the operation can end quickly, there will be no significant impact. But this is not always the case. Ideally, as long as there is no View observing the ViewModel, then the ViewModel should be destroyed.

There are many ways to do this:

  • Through the ViewModel's onCleared () method, you can tell the repository layer to drop the callback to the ViewModel.
  • In the repository, you can use weak references or you can use EventBus (both are easily misused or even considered harmful)
  • Like using LiveData to interact between the View and ViewModel, using LiveData to interact between the ViewModel and the repository layer

✅: It is necessary to consider the impact of boundary conditions, leakage and time-consuming operations on the instance objects in the architecture.

 

Use LiveData in the repository layer

To avoid leaking ViewModel and callback hell, the repository layer can be observed like this:

When the ViewModel is cleared, or the life cycle of the View ends, the subscription relationship between the ViewModel and the repository is cleared.

If you try this method, there is a problem: if you do not have access to LifecycleOwner, how do you subscribe to the repository from the ViewModel? Using Transformations is a very convenient way to solve this problem. Transformations.switchMap allows you to create a new LiveData in response to changes in other LiveData instances. It also allows to carry observer lifecycle information throughout the chain:

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

In this example, when the trigger gets an update, the function is applied and the results are scheduled downstream. Activity will observe the repo, and the same LifecycleOwner will be used for the repository.loadRepo (id) call.

✅  That is: when you need to get a Lifecycle object in ViewModel , you can use Transformations at this time

 

Expand LiveData

The usual way to use LiveData is to use MutableLiveData in the ViewModel and expose a get method.

If you need more features, Extended LiveData will notify you when there are active observers. For example, this is useful when you want to start listening to location or sensor services:

public class MyLiveData extends LiveData<MyData> {

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

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

    @Override
    protected void onInactive() {
        // Stop listening
    }
}

When shouldn't LiveData be extended

You can also use onActive () to start some services that load data, but unless you have a good reason, you do not need to wait for LiveData until it can be observed. Some common patterns:

❌You   usually don't extend LiveData. Let your Activity or Fragment tell the ViewModel when to start loading data

 

At last:

Regarding the use of ViewModel, the official website recommends a blog: ViewModels: A Simple Example

An example of using ViewModel in Android codelab: https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0

An official introduction to using ViewModel on YouTube: https://www.youtube.com/watch?v=c9-057jC1ZA

Android Jetpack: ViewModel | Chinese teaching video: https://mp.weixin.qq.com/s/uGWH1os8Kq3Pp6_x5hXI8Q

For the use of  Architecture Components , the official website gives some simple examples:

https://github.com/googlesamples/android-architecture-components/blob/master/README.md

And an example of using Jetpack recommended by the official website:   Sunflower

 

A GitHub client Github Browser Sample with Android Architecture Components using the MVVM version of life cycle components 

This is a sample app that uses Android Architecture Components with Dagger 2.

NOTE It is a relatively more complex and complete example so if you are not familiar with Architecture Components, you are highly recommended to check other examples in this repository first.

The official website introduces that this is a relatively more complicated and complete example. You need to have a certain understanding of the dependency injection framework Dagger2. It is very very very recommended to learn this example.

 

Afterwards, I will have time to write some more code and summarize the learning experience of some architectural components.

 

Published 82 original articles · Like 86 · Visit 110,000+

Guess you like

Origin blog.csdn.net/unicorn97/article/details/82151169