In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.
Views and ViewModels
Distributing responsibilities
Ideally, the ViewModels shouldn’t know anything about Android. This improves testability, leak safety, and modularity. A general rule of thumb is to make sure there are no Android.* imports in your ViewModels (exceptions such as Android.arch.*). The same applies to presenters.
- ❌ Do not let ViewModels (and Presenters) know about the Android framework classes
Conditional statements, loops, and general decisions should be made in ViewModels or other layers of the application, not in Activities or Fragments. Views generally don’t have unit tests (unless you use Robolectric), so the fewer lines of code the better. The view should only know how to display data and send user events to the ViewModel (or Presenter). This is known as passive view mode.
- ✅ keeps logic in activities and fragments to a minimum
View references in ViewModels
A viewmodel has a different scope than an Activity or Fragment. When a ViewModel is alive and running, an Activity can be in any state of its life cycle. Activities and fragments can be destroyed and created again without the ViewModel knowing.
Passing a reference to a view (Activity or Fragment) to the ViewModel is a serious risk. Let’s assume that the ViewModel requests data from the network, and the data comes back some time later. At this point, the View reference could be corrupted, or it could be an old Activity that is no longer visible, causing a memory leak and possibly a crash.
- ❌ Avoid referencing views in ViewModels.
The recommended way to communicate between ViewModels and views is in observer mode, using LiveData or observing variables from other libraries.
Observer Pattern
A very convenient way to design a presentation layer in Android is to have views (activities or fragments) observe (subscribe to) changes in the ViewModel. Since the ViewModel doesn’t know Android, it doesn’t know how Android likes to kill views frequently. This has some advantages.
- The ViewModel is persisted when configuration changes occur, so there is no need to re-query external data sources (such as databases or networks) when a re-request occurs.
- When a long-running operation ends, the observation variables in the ViewModel are updated. It doesn’t matter if the data is observed. Null pointer exceptions do not occur when an attempt is made to update a view that does not exist.
- ViewModels do not reference views, so there is less risk of memory leaks.
private void subscribeToModel() { // Observe product data viewModel.getObservableProduct().observe(this, new Observer<Product>() { @Override public void onChanged(@Nullable Product product) { mTitle.setText(product.title); }}); }Copy the code
- ✅ Do not push data to the UI, but let the UI observe its changes.
Fat ViewModels
Anything that allows you to separate your concerns is a good idea. If your ViewModel holds too much code or has too many responsibilities, consider this.
- Move some logic to presenters in the same scope as the ViewModel. It will communicate with the rest of your application and update the LiveData holder in the ViewModel.
- Add a Domain Layer and use Clean Architecture. This results in a very testable and maintainable architecture. It is also good for exiting the main thread quickly. There is an example of Clean Architecture in Architecture Blueprints.
Examples are here: 8thlight.com/blog/uncle-…
- ✅ decentralize responsibility and add a domain layer if necessary.
Using a data repository
As you can see in the Application Architecture Guide, most applications have multiple data sources, for example.
- Remote: Network or cloud
- Local: database or file
- In-memory cache
It’s a good idea to have a data layer in your application, completely unaware of your presentation layer. The algorithms that keep caches and databases in sync with the network are not easy. A single repository class is recommended as a single entry point to handle this complexity.
If you have multiple very different data models, consider adding multiple repositories.
- ✅ Add a data repository as a single point of entry for your data
Dealing with data state
Consider this scenario: You are looking ata LiveData exposed by a ViewModel that contains a list of items to display. How does a view distinguish between data being loaded, a network error, and an empty list?
You can expose a LiveData from the ViewModel. For example, MyDataState can contain information about whether data is being loaded, whether it has been successfully loaded, or failed.
You can wrap the data in a class with stateful and other metadata (such as error messages). See our sample in the resource class: developer.android.com/jetpack/gui…
- ✅ Expose the state information of your data using a wrapper or another LiveData.
Saving activity state
The Activity state is the information you need to recreate the screen when an Activity disappears, which means the Activity was destroyed or the process was killed. Rotation is the most common case, and we’ve covered this case with ViewModels. Therefore, it is safe for the state to be stored in the ViewModel.
However, you may need to restore state in other cases where the ViewModels also disappear: for example, when the operating system runs out of resources and kills your process.
To effectively save and restore UI state, you can use a combination of persistence, onSaveInstanceState(), and ViewModels.
For an example, see. ViewModels: Persistence, onSaveInstanceState(), restore UI state, and loader
Medium.com/androiddeve…
Events
An event is something that happens once. ViewModels exposes data, but what about events? For example, navigating an event or displaying a Snackbar message are actions that should only be performed once.
The concept of events does not quite match the way LiveData stores and recovers data. Consider a ViewModel with the following fields.
LiveData<String> snackbarMessage = new MutableLiveData<>();
Copy the code
An Activity starts observing this, and the ViewModel completes an operation, so it needs to update the message.
snackbarMessage.setValue("Item saved!" );Copy the code
The Activity receives the value and displays the Snackbar. It certainly works.
However, if the user rotates the phone, a new Activity is created and observed. When LiveData observation begins, the Activity immediately receives the old value, which causes the message to be displayed again.
Rather than trying to solve this problem with extensions of libraries or architectural components, confront it as a design problem. We recommend that you make your events part of your status.
- ✅ design events as part of your state. Read LiveData vs. SnackBar, Navigation, and Other events (SingleLiveEvent case) for more details.
Leaking ViewModels
The reactive paradigm works well on Android because it allows for a convenient connection between the UI and the rest of your application. LiveData is a key component of this structure, so typically your activities and fragments will observe instances of LiveData.
How the ViewModels communicates with other components is up to you, but be aware of leaks and edge conditions. Consider this diagram, where the view layer uses observer mode and the data layer uses callbacks.
If the user exits the application, the view disappears, so the ViewModel can no longer be observed. If Repository is a singleton or other scoped application, Repository will not be destroyed until the process is killed. This only happens when the system needs resources or when the user manually kills the application. If Repository holds references to callbacks in the ViewModel, the ViewModel is temporarily compromised.
If the ViewModel is lightweight, or operations are guaranteed to complete quickly, this leakage is not a big deal. However, this is not always the case. Ideally, viewModels should be free as long as no views are observing them.
You have many options to achieve this.
- By using viewModel.oncleared (), you can tell Repository to discard callbacks to ViewModel.
- In Repository, you can use WeakReference or you can use the event bus (both of which can be easily abused or even considered harmful).
- Using LiveData to communicate between repository and ViewModel is similar to using LiveData between View and ViewModel.
This can also be solved with Flow.
- ✅ Consider how edge cases, leaks, and long-running operations can affect instances in your architecture.
- ❌ Do not place logic to save clean state or data in the ViewModel. Any call you make from the ViewModel is likely to be your last.
LiveData in repositories
To avoid leaking ViewModels and callback hell, you can look at the repository like this.
Subscriptions are cleared when the ViewModel is cleared or when the view’s life cycle ends.
If you try this approach, there is a question: how do you subscribe to Repository from ViewModel if you can’t access LifecycleOwner? A quick way to do this is to make yourself look quick. Transformations. SwitchMap lets you create a new LiveData, to respond to the change of other LiveData instance. It also allows the lifecycle information of the observer to be carried along the chain.
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> { if (repoId.isEmpty()) { return AbsentLiveData.create(); } return repository.loadRepo(repoId); });Copy the code
In this example, when the trigger is updated, the function is applied and the results are sent downstream. An Activity will observe the repO, and the same LifecycleOwner will be used for the repository.loadRepo(ID) call.
As long as you think you need a Lifecycle object in the ViewModel, a Transformation might be the solution.
Extending LiveData
The most common use case for LiveData is to use MutableLiveData in ViewModels and expose them as LiveData, making them immutable from the observer.
If you need more functionality, extending LiveData will let you know when there are active observers. This is useful when you want to start listening to a location or sensor service, for example.
public class MyLiveData extends LiveData<MyData> {
public MyLiveData(Context context) {
// Initialize service
}
@Override
protected void onActive() {
// Start listening
}
@Override
protected void onInactive() {
// Stop listening
}
}
Copy the code
When not to extend LiveData
You can also use onActive() to start some services that load data, but unless you have a good reason, you don’t need to wait for LiveData to observe. Some common patterns. Add a start() method to the ViewModel and call it as soon as possible: github.com/android/arc…
Set a startup load property: github.com/android/arc…
- ❌ You don’t normally extend LiveData. Have your Activity or Fragment tell the ViewModel when to start loading data.
Original link: medium.com/androiddeve…
I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit