This is my implementation:
class SampleFragment : Fragment()
{
private val viewModel by sharedViewModel<MyViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
FragmentSampleBinding.inflate(inflater, container, false).apply {
myButton.setOnClickListener { viewModel.foo() }
viewModel.statusLiveData.observe(viewLifecycleOwner, Observer {
titleTextView.text = it.status.title
myButton.text = it.status.button_title
})
}.root
}
In rare occations, this crash happens:
java.lang.NullPointerException:
at com.example.fragments.SampleFragment$onCreateView$1$2.onChanged (SampleFragment.kt:2)
at com.example.fragments.SampleFragment$onCreateView$1$2.onChanged (SampleFragment.kt:1)
at androidx.lifecycle.LiveData.considerNotify (LiveData.java:6)
at androidx.lifecycle.LiveData.dispatchingValue (LiveData.java:5)
at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged (LiveData.java:10)
at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged (LiveData.java:3)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent (LifecycleRegistry.java:3)
at androidx.lifecycle.LifecycleRegistry.forwardPass (LifecycleRegistry.java:9)
at androidx.lifecycle.LifecycleRegistry.sync (LifecycleRegistry.java:9)
at androidx.lifecycle.LifecycleRegistry.moveToState (LifecycleRegistry.java:5)
at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent (LifecycleRegistry.java:2)
at androidx.fragment.app.FragmentViewLifecycleOwner.handleLifecycleEvent (FragmentViewLifecycleOwner.java:1)
at androidx.fragment.app.Fragment.performStart (Fragment.java:9)
at androidx.fragment.app.FragmentStateManager.start (FragmentStateManager.java:3)
at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:28)
at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:69)
at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState (FragmentManager.java:4)
at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:75)
at androidx.fragment.app.BackStackRecord.executeOps (BackStackRecord.java:28)
at androidx.fragment.app.FragmentManager.executeOps (FragmentManager.java:6)
at androidx.fragment.app.FragmentManager.executeOpsTogether (FragmentManager.java:14)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute (FragmentManager.java:10)
at androidx.fragment.app.FragmentManager.execPendingActions (FragmentManager.java:4)
at androidx.fragment.app.FragmentManager$4.run (FragmentManager.java:1)
at android.os.Handler.handleCallback (Handler.java:751)
at android.os.Handler.dispatchMessage (Handler.java:95)
at android.os.Looper.loop (Looper.java:154)
at android.app.ActivityThread.main (ActivityThread.java:6823)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:1563)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1451)
One idea is that this happens when the view is destroyed before the observe() subscription has ended.
I dug into the fragment code and found this in FragmentManager:
private void destroyFragmentView(@NonNull Fragment fragment) {
fragment.performDestroyView();
mLifecycleCallbacksDispatcher.dispatchOnFragmentViewDestroyed(fragment, false);
fragment.mContainer = null;
fragment.mView = null;
// Set here to ensure that Observers are called after
// the Fragment's view is set to null
fragment.mViewLifecycleOwner = null;
fragment.mViewLifecycleOwnerLiveData.setValue(null);
fragment.mInLayout = false;
}
The viewLifecycleOwner actually is destroyed after the view has been destroyed. But since this function is non-suspendable, there should be no way of interleaving from the main thread.
Is my assumption correct?
I further found that the viewLifecycleOwner can live long if there is an exit transition (FragmentManager#moveToState() Lines 1261+):
// If a fragment has an exit animation (or transition), do not destroy
// its view immediately and set the state after animating
if (mExitAnimationCancellationSignals.get(f) == null) {
destroyFragmentView(f);
} else {
f.setStateAfterAnimating(newState);
}
However this would also delay the destruction of the view.
Do you have any ideas how the crash happens?
(I left the Koin injection of sharedViewModel in the sample in case this could be the source of a problem.)