Android module development framework LiveData+ViewModel
preface
Why choose LiveData+ViewModel
- LiveData+ViewModel is part of the Android Architecture Component development Component. The main purpose is to solve some common problems caused by the Activity and Fragment life cycle during Android development. For example: Memory leaks, null Pointers caused by asynchronous tasks, horizontal and vertical switching interface refresh issues, of course, it does more than that, such as: The LiveData observer model ensures that the interface is updated to the latest data as soon as possible (of course, your LifecycleOwner must be Alive), which solves the synchronization problem of multiple writes. Use LiveData to automatically bind the View to the VM (usually the data flow of this binding is one-way, VM->View). It is also worth noting that the AAC framework internally maintains a memory cache pool of viewModels and listens for Activity or Fragment life cycles to automatically empty the cache during destory. As a result, there is little need for developers to interface with the lifecycle and focus on business development.
- Interested students can go to the official detailed documentation and Demo
- Official documents: Links
- Official Demo: Link
MVP or MVVM?
- The biggest difference between MVVM and MVP is the automatic Binding of V and VM (P). There are many interface dependencies between V and P in MVP, which is not conducive to expansion and testing. MVVM usually has a Binding intermediary layer, which removes the direct interface dependencies between V and VM through annotations +apt (or reflection). Of course, MVVM’s improvement over MVP is not only code decoupling, but also a shift in thinking from “functional interface programming” to “responsive programming”, where everything is data (instruction) flow (UI <-> data <->model).
- The official recommendation is to use the MVVM framework in conjunction with the DataBinding dependency injection framework to implement bidirectional binding between View and VM. Considering that using DataBinding relies on XML layout configuration and has a high cost of understanding, we chose not to use the strict MVVM framework this time, but to choose a compromise solution:
- VM->View: One-way flow of data through LiveData
- View->VM: Still use the traditional interface implementation, but all execution results rely on LiveData back to the View
The classic principle of dependency
- The quality of a framework is usually measured by:
- Whether the current service problem can be solved
- Does it have good scalability
- Does it have good testability
- Whether modular design principles are followed
- Logic, interface, data separation
- Of course, there are more indicators, we do not list one by one here, as shown in the figure above is a ring dependence structure, from the inside to the outside, respectively: business data -> business logic layer -> interface adaptation layer -> interface, following the “dependency inversion principle”, the internal circle cannot depend on the external circle
The framework is introduced
The internal hierarchy of a module
- In accordance with the principle of one-way dependence, our module is divided into three levels, from bottom to top:
- Data layer:
- It is mainly used to provide the data needed for interface display and interaction. It usually defines the policy interface for acquiring data and selects different implementations (DB, memory, network, etc.).
- Not dependent on other layers, dependent on logical layers
- Logical layer (Domain layer)
- This layer is highly business-related and contains complex business logic, such as data acquisition, data submission, data storage policy selection and data fusion
- Depend on the data layer, be dependent on the presentation layer
- Display layer (Presentation layer)
- The main tasks of this layer are as follows:
- Build user visible interfaces
- Provide the necessary data for the interface presentation
- Receive and process user interaction events
- Complex business logic is delegated to the logic layer (the domain layer), where the ViewModel can be thought of as an interface adapter that establishes a communication channel with the View and then passes data or receives instructions, not complex business logic on its own
- Dependency logic layer (domain layer)
- The main tasks of this layer are as follows:
- Data layer:
Introduction to each level
Demo layer: One-way binding of MVVM using LiveData
- There are two kinds of communication between a View and a VM
- View->VM, usually some instructions generated by user interaction (may carry some data, for example: user login will carry account password)
- VM->View, usually the data the interface needs to display (it can also be a state, e.g. failed to load data, showing a Toast prompt, etc.)
- Let’s take a simple example, a list interface, to refresh the data and display, there are several necessary steps:
- First, the View holds an instance of the ViewModel (instantiated itself, or passed externally).
- Get a LiveData object from the ViewModel (there can only be one instance of the same class of LiveData in the ViewModel) and start observing the LiveData object (commonly known as SUBSCRIBE).
- The ViewModel receives the “refresh data” command and delegates it to the specific UseCase
- UseCase gets the data from the data source and writes it to LiveData
- LiveData notifies all observers (of course, determining whether the LifecycleOwner to which the observer is attached is alive), including the View
- View retrieves the latest complete data list from LIveData and refreshes the display interface
Logic layer: UseCase handles complex logic
- As mentioned earlier, usecase is primarily used to handle complex business logic and lighten the ViewModel load
- BaseUseCase can be thought of as a template method class (of course, this template may not be suitable for all business scenarios). It does some non-business related operations such as “thread scheduling” and “LiveData assignment” internally. The specific business logic is implemented by subclasses
- There is a Either
,>
return value, which is a Java 8 functional programming feature, similar to THE C language’s Union, used to return two (or more) values in a type-safe manner. You can Google this for yourself
Data layer: Repository policy
- Define a policy interface to obtain (read/write) data, implement different data read/write policies, or a combination of multiple policies, based on specific service scenarios. The biggest advantage is scalability, and the logical layer (domain layer) does not care about where the data comes from
Use guide
How to define an independent submodule
- There are two typical ideas for module division: “module division according to function purpose” and “module division according to business characteristics”. A common practice of the former is to group files according to Model, View, Present(Controler) and other roles. The biggest disadvantage of this method is that it is not conducive to business division and multi-person collaborative programming. We recommend “business feature module”, for example: the main interface, details page, login page, etc are a relatively independent module
- Then, how to define an independent submodule needs to meet the following conditions:
- Relatively independent interface display (an Activity or Fragment in Android)
- Relatively independent data sources (the data needed for your interface rendering can be obtained from a separate data warehouse, e.g. a separate server API, a separate data table)
- The impact of user interaction should be kept within the interface as much as possible (e.g., data generated by a pull-down refresh is only used to render the current page).
- Have a closed loop lifecycle (memory used by modules is recyclable, singletons are not recommended for cross-module memory sharing)
- In a nutshell, a module is a relatively independent module if it can run independently of other modules in the default manner
Build a submodule
- Let’s take a list interface as an example.
- Follow these steps to develop
- Step1 data layer: data warehouse implementation
- Defining data Beans
public class HotContentItem { public String id; public String name; public String desc; public long timeStamp; } Copy the code
- Data warehouse policy implementation (data is mock locally)
public class HotContentNetRepository { / / the mock data public Either<? extends Failure, List<HotContentItem>> refreshNew() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } Either<? extends Failure, List<HotContentItem>> result; Random random = new Random(); boolean success = random.nextInt(10) > 3; if (success) { result = Either.right((mockItemList(0))); } else if (random.nextInt(10) > 3) { result = Either.right(Collections.<HotContentItem>emptyList()); } else { result = Either.left(new NetworkFailure()); } returnresult; }}Copy the code
- Step2 Logical layer: data warehouse selection and use
- Omit the column step and implement different data warehouse combinations based on business requirements
- Step3 Logical layer: Implement UseCase (example code: refresh data)
public class HotContentRefreshNew extends BaseUseCase<List<HotContentItem>, Void> { private HotContentNetRepository mNetRepository; public HotContentRefreshNew( MutableLiveData
-
> data, MutableLiveData
failure) { super(data, failure); mNetRepository = new HotContentNetRepository(); } @Override protected Either<? extends Failure, List<HotContentItem>> loadData(Void aVoid) { // Get data from the network Either<? extends Failure, List<HotContentItem>> result = mNetRepository.refreshNew(); if (result.isRight() && CollectionUtil.isEmpty(result.right())) { Failure failure = new RefreshNewFailure(RefreshNewFailure.CODE_DATA_EMPTY, "Data is empty!"); result = Either.left(failure); } return result; } @Override protected Failure processFailure(Failure failure) {... }}Copy the code - Step4 display layer: UI frame selection
- The sample interface is a Page as a TabLayout, so the UI framework selected here is “With lifecycle View”, which is a custom View, Implementation of LifecycleOwner interface (reference LifecycleActivity and LifecycleFragment implementation logic)
public abstract class BaseLifecycleView extends FrameLayout implements LifecycleOwner { private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); private ViewModelStore mViewModelStore = new ViewModelStore(); public BaseLifecycleView(@NonNull Context context) { super(context); } protected abstract void onCreate(a); protected abstract void onDestroy(a); @Override public Lifecycle getLifecycle(a) { return mRegistry; } @Override @CallSuper protected void onAttachedToWindow(a) { super.onAttachedToWindow(); mRegistry.handleLifecycleEvent(Event.ON_CREATE); onCreate(); if(getVisibility() == View.VISIBLE) { mRegistry.handleLifecycleEvent(Event.ON_START); }}@Override @CallSuper protected void onDetachedFromWindow(a) { super.onDetachedFromWindow(); mRegistry.handleLifecycleEvent(Event.ON_DESTROY); mViewModelStore.clear(); onDestroy(); } @Override @CallSuper protected void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); Event event = visibility == View.VISIBLE ? Event.ON_RESUME : Event.ON_PAUSE; mRegistry.handleLifecycleEvent(event); } @Override @CallSuper public void onStartTemporaryDetach(a) { super.onStartTemporaryDetach(); State state = mRegistry.getCurrentState(); if(state == State.RESUMED) { mRegistry.handleLifecycleEvent(Event.ON_STOP); }}@Override @CallSuper public void onFinishTemporaryDetach(a) { super.onFinishTemporaryDetach(); State state = mRegistry.getCurrentState(); if(state == State.CREATED) { mRegistry.handleLifecycleEvent(Event.ON_START); }}protected <T extends ViewModel> T getViewModel(@NonNull ViewModelProvider.NewInstanceFactory modelFactory, @NonNull Class
modelClass) { return newViewModelProvider(mViewModelStore, modelFactory).get(modelClass); }}Copy the code - Step5 Display layer: define your own LiveData and ViewModel
public class HotContentViewModel extends BaseViewModel<List<HotContentItem>> { private HotContentRefreshNew mRefreshNew; public HotContentViewModel(a) { refreshNew(); } public void refreshNew(a) { AssertUtil.mustInUiThread(); if (mRefreshNew == null) { mRefreshNew = new HotContentRefreshNew(getMutableLiveData(), getMutableFailure()); } // Perform specific refresh operations through usecase mRefreshNew.executeOnAsyncThread(null); }... }Copy the code
- Step6 display layer: associate V and VM
public class HotContentView extends BaseLifecycleView { private HotContentViewModel mViewModel; private SwipeRefreshLayout mSwipeRefreshLayout; private AutoLoadMoreRecycleView mRecyclerView; private HotContentAdapter mContentAdapter; public HotContentView(@NonNull Context context) { super(context); View object initialization... mRecyclerView.setLoadMoreListener(new LoadMoreListener() { @Override public void onLoadMore(a) { HotContentItem lastOne = CollectionUtil.lastOne(mViewModel.getData().getValue()); if (lastOne == null) { mRecyclerView.completeLoadMore("No more data"); } else{ mViewModel.loadHistory(lastOne); }}}); . mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh(a) { // Refresh datamViewModel.refreshNew(); }}); }@Override protected void onCreate(a) { mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class); mViewModel.getData().observe(this.new Observer<List<HotContentItem>>() { @Override public void onChanged(@Nullable List<HotContentItem> hotContentItems) { // Succeeded in refreshing data mContentAdapter.setItemList(hotContentItems); mSwipeRefreshLayout.setRefreshing(false); . }}); . }@Override protected void onDestroy(a) {}}Copy the code
- Step1 data layer: data warehouse implementation
Problems encountered
- How are complex UI interaction instructions communicated to the ViewModel
- As mentioned in the “MVP or MVVM” framework selection at the beginning of this article, the essence of MVVM is not currently “DataBinding”, but one-way binding of V->VM via LiveData Observer mode (i.e. : Data can flow from VM to V automatically, but THE operation instructions of V cannot be automatically passed to VM), so complex interactions (such as pull-down refresh, scroll load more) still need to be defined in VM for V to invoke through traditional MVP thinking
- In addition to data, there are states that affect the presentation of the interface
- Ideally, the VM provides the View with a LiveData that contains all the data the View needs to render, but in many cases the View does not rely on a single type of data. For example, a drop-down refresh operation will return three results: list data, empty data, and failure. For “list data” we can notify the View to refresh the whole through LiveData, but “empty data” and “failure” also need to be prompted on the interface, and these two return values can not affect the current “list data” (i.e. : Do not affect the current list display), but should be regarded as independent and data outside the “instructions”, their biggest characteristic is “one-time”, do not need to be stored and processed like “list data” (can be understood as a one-time event for interface consumption).
- Going back to LiveData, LiveData is mainly used to store relatively persistent data, and at any time the View obtains data from LiveData that must be “complete” and ready to render directly. Going back to the “drop-down refresh” example above, If we encapsulate “empty data” and “failed” with LiveData, and then let the View observe the LiveData (a custom Observer), and handle the “interface prompt” when it receives the corresponding “instruction” notification, it would also satisfy the VM->View’s status notification requirement. Since the lifetime of an Observer is likely to be shorter than the lifetime of LiveData (depending on the LifecycleOwner the Observer relies on) (e.g. : When a View is reused, it will observe the same LiveData again and automatically receive notification from the LiveData. Get the latest LiveData (e.g., “failed” command), refresh the interface (” failed to refresh “), and it will be very strange to get “failed to refresh” when there is no refresh action.
- The solution is to return to the characteristics of the command “DisposableLiveData”, define a DisposableLiveData, and set it to DisposableLiveData immediately after each execution of setData (which will notify the viewer, that is, View), so that the next time getData returns null, instead of an “unexpected data”.
- The code implementation is simple
public class DisposableLiveData<T> extends MutableLiveData<T> { @Override public void postValue(T value) { super.postValue(value); if(value ! =null) { super.postValue(null); }}@Override public void setValue(T value) { super.setValue(value); if(value ! =null) { super.postValue(null); }}Copy the code
- Sample code:
public class HotContentView extends BaseLifecycleView { private HotContentViewModel mViewModel; private SwipeRefreshLayout mSwipeRefreshLayout; public HotContentView(@NonNull Context context) { super(context); . mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh(a) {    // Refresh datamViewModel.refreshNew(); }}); }@Override protected void onCreate(a) { mViewModel = getViewModel(newNewInstanceFactory(), HotContentViewModel.class);   . mViewModel.getFailure().observe(this.new Observer<Failure>() { @Override public void onChanged(@Nullable Failure failure) { // Processing failure message if (failure instanceof RefreshNewFailure) { mSwipeRefreshLayout.setRefreshing(false); ToastManager.getInstance().showToast(getContext(), ((RefreshNewFailure)failure).getMessage(), Toast.LENGTH_SHORT); }... }}); }@Override protected void onDestroy(a) {}}Copy the code