Project experience, if necessary, please specify the author: Yuloran (t.cn/EGU6c76)
preface
This paper mainly describes the advantages and pain points of MVP and MVVM of Android App architecture, and does not introduce specific implementation methods. Because of its simple architecture, THE MVP needs no introduction. MVVM architecture is relatively complex, with LifecycleOwner, LifecycleObserver and LifecycleRegistry components at its core. Google has also developed DataBinding, ViewModel, and LiveData to implement the full MVVM architecture. Related components have been incorporated into JetPack Architecture. For specific practice, please refer to my open source project WanAndroid_java, which is a minimization practice of MVVM architecture, so as to facilitate understanding and exploring the implementation principle behind MVVM architecture.
MVP
View -> Presenter -> Model:
The core of this architecture is that both View and Presenter are abstracted as interfaces, an interface oriented programming practice. Because of the interface orientation, dependency isolation is implemented, meaning that you do not need to know the specific Class type.
advantages
- The UI and business logic can be developed independently by different development teams only if the View and Presenter interfaces are well defined
- Business logic is maintained only in Presenter, following the design principle of a single responsibility class, which improves code maintainability
- The interface request and cache policies are maintained only in Model, as in Presenter, following a single responsibility class design principle that improves code maintainability
- It avoids the problem of the Controller class being too big in MVC architecture. In the MVP architecture, the View, Presenter, and Model classes are not very large, which improves the readability and maintainability of code
Pain points
This is not the pain point of MVP architecture, but the pain point of Android App development as a whole. That is, UI operations must be performed within the Activity and Fragment lifecycle, preferably after onStart() and before onPause(). Otherwise, it is extremely prone to all kinds of anomalies. In the MVP architecture, Presenter is insensitive to Activity and Fragment life cycles, so you need to manually add the corresponding life cycle methods and perform special handling to avoid exceptions or memory leaks.
MVVM
In fact, according to the position of the code in the App, it is more appropriate to describe VVMM, namely View -> ViewModel-> Model:
At the heart of this architecture are viewModels and LiveData. The ViewModel is used to ensure that data is not lost when a device recreates a FragmentActivity due to a configuration change (currently the ViewModel only supports FragmentActivity and Fragment). LiveData is used to make sure that the onChanged(T data) callback is only called if the FragmentActivity or Fragment’s lifecycle state is [onStarted, onResumed], So we can safely update the UI in onChanged(). Here is a brief introduction to the source code is how to achieve:
The ViewModel does not reconstruct the principle
- Save the ViewModel instance:
/**
* Retain all appropriate fragment state. You can NOT
* override this yourself! Use {@link #onRetainCustomNonConfigurationInstance()}
* if you want to retain your own state.
*/
@Override
public final Object onRetainNonConfigurationInstance(a) {
Object custom = onRetainCustomNonConfigurationInstance();
FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
if (fragments == null && mViewModelStore == null && custom == null) {
return null;
}
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = mViewModelStore;
nci.fragments = fragments;
return nci;
}
Copy the code
- Restore ViewModel instance:
/** * Perform initialization of all fragments. */
@SuppressWarnings("deprecation")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if(nc ! =null&& nc.viewModelStore ! =null && mViewModelStore == null) {
mViewModelStore = nc.viewModelStore;
}
// omit the irrelevant codes. }Copy the code
By the above code can be seen, in the onCreate (), if getLastNonConfigurationInstance () is not null, will mViewModelStore viewModelStore recovery for members of these variables, MViewModelStore caches an instance of ViewModel internally:
public class ViewModelStore {
// Key is a fixed prefix followed by the class name of the ViewModel implementation class, and value is the implementation class instance
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if(oldViewModel ! =null) { oldViewModel.onCleared(); }}final ViewModel get(String key) {
return mMap.get(key);
}
/** * Clears internal storage and notifies ViewModels that they are no longer used. */
public final void clear(a) {
for(ViewModel vm : mMap.values()) { vm.onCleared(); } mMap.clear(); }}Copy the code
LiveData security update principle
- Register the Observer and listen for LifecycleOwner lifecycle changes. The first parameter is LifecycleOwner (an interface that indicates that a class has a life cycle). The current implementation classes are FragmentActivity and Fragment. The Fragment in the article refers to the Fragment in the Support library. Currently, Google has migrated it to the androidx.* package, and the Fragment in the Android.app.* package has been marked as Deprecated. Not recommended)
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
Encapsulate the input observer as LifecycleBoundObserver, which is a common way of writing and one of the design principles: use composition more and inheritance less
// For the sake of distinction, I call the observer inner Observer
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if(existing ! =null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if(existing ! =null) {
return;
}
// Register into Lifecycle to listen for Lifecycle changes
owner.getLifecycle().addObserver(wrapper);
}
Copy the code
- LifecycleBoundObserver that listens for life cycle changes
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull
final LifecycleOwner mOwner;
LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
super(observer);
mOwner = owner;
}
/** * Only STARTED or RESUMED returns true */
@Override
boolean shouldBeActive(a) {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
/** * when the life cycle changes, all callback */
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
// Destroy removes the Inner Observer
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}
@Override
void detachObserver(a) {
mOwner.getLifecycle().removeObserver(this); }}Copy the code
The parent class:
private abstract class ObserverWrapper {
final Observer<? super T> mObserver;
boolean mActive;
int mLastVersion = START_VERSION;
ObserverWrapper(Observer<? super T> observer) {
mObserver = observer;
}
abstract boolean shouldBeActive(a);
boolean isAttachedTo(LifecycleOwner owner) {
return false;
}
void detachObserver(a) {}void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this); }}}Copy the code
We see that onStateChanged() is called back each time the lifecycle changes. Remove inner Observer if destroy, otherwise activeStateChanged(shouldBeActive()) callback assigns the return value of shouldBeActive() to mActive. ShouldBeActive () only returns true if the life cycle is STARTED or RESUMED, and onChanged only if mActive is true:
private void considerNotify(ObserverWrapper observer) {
if(! observer.mActive) {return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if(! observer.shouldBeActive()) { observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}
Copy the code
Because LiveData supports non-UI thread updates to mData (postValue() method), version numbers were introduced to solve the ABA problem.
advantages
- ViewModel: When the Activity is rebuilt due to a device configuration change, there is no need to reload data from the Model, reducing IO operations
- LiveData: Don’t worry about the life cycle when updating the UI
- Data Binding: Reduces the need to write template code, and bidirectional Binding is already supported (note: Not all UIs need to use Data Binding, although we can really “do whatever we want” with @bindingAdapter, it is best to use it only for layouts that require Binding Bean classes.)
disadvantages
- In the process of actually writing the App, before showing the final data, you need to show the transition UI, such as a progress bar, a cool animation, an exception notification when loading fails, etc., which can be called ViewState. In the MVP architecture, these state updates are defined in the View interface. But the MVVM architecture is not covered. So we need to wrap our own ViewData class to encapsulate the data class and ViewState, like this:
/**
* [UI State: init->loading->loadSuccess|loadFailure]
* <p>
* Author: Yuloran
* Date Added: 2018/12/20 20:59
*
* @param<T> The data type required by the UI *@since1.0.0 * /
public class BaseViewData<T>
{
@NonNull
private ViewState viewState;
private T viewData;
public BaseViewData(@NonNull ViewState viewState)
{
Objects.requireNonNull(viewState, "viewState is null!");
this.viewState = viewState;
}
public BaseViewData(@NonNull T viewData)
{
this.viewState = ViewState.LOAD_SUCCESS;
this.viewData = viewData;
}
@Nullable
public T getViewData(a)
{
return viewData;
}
public void setViewData(@NonNull T data)
{
this.viewData = data;
}
@NonNull
public ViewState getViewState(a)
{
return viewState;
}
public void setViewState(ViewState viewState)
{
this.viewState = viewState; }}Copy the code
This makes it easy to update the UI via LiveData.
-
Because LiveData uses observer mode, avoid circular calls. For example, if an interface request fails, the LiveData callback fails, and a new interface request is initiated in the callback. If interface requests continue to fail and no special processing is done, circular calls will occur, which will consume a lot of CPU resources and degrade the performance of the App.
-
LiveData receives a lifecycle change callback each time as long as the Inner Observer mActive is true and mLastVersion! = mVersion, both call back onChanged(), which can be problematic in some situations, such as popping up a dialog box in onChanged() or jumping to another page. Imagine this scenario: The Activity is rebuilt because of a device configuration change, so when the Activity goes to onStart(), LiveData calls onChanged(). This is because after the Activity is rebuilt, even though the ViewModel is not rebuilt, However, the Inner Observer of LiveData is re-registered, so the mLastVersion of this observer! = mVersion. According to the source code above, onChanged must be called back at this time, resulting in a wrong pop-up dialog or a wrong jump to another page. The SingleLiveEvent in Google Samples can be used to avoid this problem.
-
LiveData itself has no public methods, so we should use its subclass MutableLiveData. In this design, we can use MutableLiveData in the Model, and in the ViewModel, only provide LiveData to the View, avoiding the View updating the LiveData. I have also written a class, SafeMutableLiveData, that automatically calls setValue() or postValue() and provides a default value for getValue().
-
Some scenarios, such as network connectivity listening, only require the onChanged() callback if there is an actual change, which is currently not possible with LiveData. In onCreate(), once you add an observer to LiveData, as soon as you go to onStart(), onChanged() will be called back and the last mData will be sent, without actually changing the network connection. Therefore, this scenario requires special treatment.
conclusion
The purpose of the architecture is to improve the readability and maintainability of the code, not to excessively increase the complexity of the code, let alone arbitrarily introduce such and such frameworks. Google has also officially said that there is no universal architecture. If the project is already stable, there is no need to re-architect. But as a developer, you still have to be sensitive to new technologies.
The attached
- Lifecycle.State
- ViewModel Lifecycle
The image is from Google Android