First of all, to admit that this series is a bit of a headline bash, Jetpack’s MVVM itself is not to blame, but some misuse by the developers. This series will share some common MISTAKES in AAC and guide you to a healthier application architecture

Fragment as LifecycleOwner problem

The core of MVVM is a data-driven UI. In Jetpack, this idea is reflected in the following scenario: the Fragment subscribing to LiveData in the ViewModel to drive its own UI updates

As for the timing of the subscription, it is generally chosen to place it in onViewCreated, as follows:

override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)

        viewModel.liveData.observe(this) { // Warning: Use fragment as the LifecycleOwner
           updateUI(it) 
        } 

}
Copy the code

We know that when you subscribe to LiveData, you need to pass in the LifecycleOwner to prevent leaks. One easy mistake to make is to use a Fragment as the LifecycleOwner, which in some scenarios can cause duplicate subscription bugs.

Here’s an experiment:

val handler = Handler(Looper.getMainLooper())

class MyFragment1 : Fragment() {
    val data = MutableLiveData<Int> ()override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)

        tv.setOnClickListener {
            parentFragmentManager.beginTransaction()
                .replace(R.id.container, MyFragment2())
                .addToBackStack(null)
                .commit()
                
            handler.post{ data.value = 1}}data.observe(this, Observer {
            Log.e("fragment"."count: ${data.value}")})}Copy the code

When jumping to MyFragment2 and then returning MyFragment1, it prints two logs

E/fragment: count: 1
E/fragment: count: 1
Copy the code

Cause analysis,

LiveData protects against leaks by removing the Observer associated with the LifecycleOwner when the LifecycleOwner lifecycle is DESTROYED

//LiveData.java

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
   if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
          removeObserver(mObserver);
          return;
   }
   activeStateChanged(shouldBeActive());
   
}
Copy the code

In the previous example, a page jump based on FragmentManager#replace causes MyFragment1 to be bumped/pushed from the BackStack. Because of the reuse of the Framgent instance, onDestroy did not occur, but the Fragment’s View reconstruction caused onCreateView to be re-created, which caused the Observer to be added twice, but without the corresponding remove.

Lifecycle is not consistent with Fragment#mView’s Lifecycle. When we subscribe to LiveData, we use LivecycleOwner. So in any replace based page switching scenario, such as ViewPager, Navigation, etc

The solution

Once you understand the cause of the problem, the solution becomes clear: you must ensure that the timing of the subscription matches the LifecycleOwner you are using, that is, either adjust the timing of the subscription or change the LifecycleOwner

Subscribe in onCreate

The first idea is to change the timing of the subscription, so that the subscription is moved up to onCreate, so that it can be paired with onDestory, but unfortunately this creates another problem.

When a View is rebuilt by a Fragment entering or leaving the stack, we need the rebuilt View to display the latest state. However, since the subscribed Observer in onCreate has already obtained the latest Value of LiveData, Obsever cannot be notified again if there is no new change in the Value

In the LiveData source code, the body now notifishes Obsever of mLastVersion:

//LiveData.java

    private void considerNotify(ObserverWrapper observer) {
        if(! observer.mActive) {return;
        }
         
        if(! observer.shouldBeActive()) { observer.activeStateChanged(false);
            return;
        }
        if (observer.mLastVersion >= mVersion) {// Value is in the latest version
            return;
        }
        
        observer.mLastVersion = mVersion;
        //noinspection unchecked
        observer.mObserver.onChanged((T) mData);
    }
Copy the code

It is in order to ensure that the reconstructed View is updated with the latest data that we complete the subscription in onViewCreated. So there is only one alternative to consider, replacing LifecycleOwner

Using ViewLifecycleOwner

Since support-28 or AndroidX-1.0.0, the Fragment getViewLifecycleOwner method has been added. As the name implies, it returns a LifecycleOwner that matches the Fragment#mView. You can go through the onDestroyView and delete the Observer registered with the onCreateView. The add/remove pairing is guaranteed.

Looking at the source code, the principle is very simple

//Fragment.java
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        / /...
        
        mViewLifecycleOwner = new LifecycleOwner() {
            @Override
            public Lifecycle getLifecycle(a) {
                if (mViewLifecycleRegistry == null) {
                    mViewLifecycleRegistry = new LifecycleRegistry(mViewLifecycleOwner);
                }
                returnmViewLifecycleRegistry; }}; mViewLifecycleRegistry =null;
        mView = onCreateView(inflater, container, savedInstanceState);
        if(mView ! =null) {
            // Initialize the LifecycleRegistry if needed
            mViewLifecycleOwner.getLifecycle();
           // Then inform any Observers of the new LifecycleOwner
            mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner); / / mViewLifecycleOwnerLiveData the schematics
        } else {
            / /...}}Copy the code

Create mViewLifecycleOwner based on mViewLifecycleRegistry,

     @CallSuper
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {// called when onCreateView
        if(mView ! =null) { mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); }}@CallSuper
    public void onDestroyView(a) {
        if(mView ! =null) { mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); }}Copy the code

Then push to the appropriate lifecycle on onCreateView and onDestroyView.

getViewLifecycleOwnerLiveData

By the way, and getViewLifecycleOwner along with new getViewLifecycleOwnerLiveData. From the front of the source code of the use of mViewLifecycleOwnerLiveData, should be able to guess the role of: This is the implementation of idea 1 discussed above. Even if you subscribe in onCreate, the reconstructed View can update the data because the LiveData is reset in onCreateView.

  // Then inform any Observers of the new LifecycleOwner
  mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);
Copy the code

Need special attention is, according to the MVVM best practice, we hope that by the ViewModel rather than hold LiveData fragments, it is no longer recommended getViewLifecycleOwnerLiveData

Finally: StateFlow vs. lifecycleScope

LiveData is used as an example to introduce the use of ViewLifecycleOwner, and now more and more people are beginning to use coroutine StateFlow. Likewise, be careful not to use LifecycleOwner incorrectly

CoroutineScope is required to subscribe to StateFlow, and AndroidX provides a lifecycleOwner-based extension method

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
Copy the code

When we get the lifecycleScope in the Fragment, remember to use the ViewLifecycleOwner

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)

        // Use the viewLifecycleOwner lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.someDataFlow.collect {
                    updateUI(it)
                }
            }
        }
    }
}
Copy the code

Notice that there is a repeatOnLifecycle(…) “Is not relevant to this article, but will involve the second crime plot, stay tuned.