How to correctly share data between Fragements over ViewModel?

Issue

In my app I use Navigation Component and also, use ViewModel to share data between Fragments. This is my scenario, Fragment A, Fragment B, and Fragment C. A shares string data with B and B shares it with C, where as in C the user edit the shared string and goes back to B with edited string data. I share data over a ViewModel class like this:

class Share : ViewModel() {
    val shared  MutableLiveData<String>()

    fun share(share: String?) {
        shared.value  share!!
    }
}

From A I send the string to B like this

...

private val share: Share by activityViewModels()

...

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

        ...
        // Let's say, this is executed in some onClickListener {}
        share.share("My String for B!")
        findNavController().navigate(R.id.action_nav_a_to_nav_b)
        ...
}
...

According to Google, to receive shared string in B, I have to observe data like this:

...
private var sharedString  ""
private val share: Share by activityViewModels()

...

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    ...
    share.shared.observe(viewLifecycleOwner, { // shared String })
    ...
}

First problem:

I cannot access the observe to get the string like this:

...
var sharedString  ""
share.shared.observe(viewLifecycleOwner, { 
    sharedString  it // The value is not assigned!
})
println(sharedString) // This prints nothing!
...

Therefore, I have to call a method to be able to access it:

share.shared.observe(viewLifecycleOwner, { get(it) })

...

private fun get(string: String) {
    println(string) // This prints the shared string "My String for B!"!
}

The next problem is that, according to that approach, sometimes app crashes:

java.lang.NullPointerException: 
  at com.myapp.BFragment.get (BFragment.java:45) // -> this is the line of share.shared.observe(viewLifecycleOwner, { get(it) })
  at com.myapp.BFragment.onViewCreated$lambda-3 (BFragment.java:25) // -> this is the line of  private val share: Share by activityViewModels()
  at com.myapp.BFragment$$InternalSyntheticLambda$0$e6af021286f0d4217981b099cd8f3b0ac57de6c0ebe35bee4010f87e07a6ecac$2.onChanged (BFragment.java)
  at androidx.lifecycle.LiveData.considerNotify (LiveData.java:133)
  at androidx.lifecycle.LiveData.dispatchingValue (LiveData.java:146)
  at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged (LiveData.java:468)
  at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged (LiveData.java:425)
  at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent (LifecycleRegistry.java:354)
  at androidx.lifecycle.LifecycleRegistry.forwardPass (LifecycleRegistry.java:265)
  at androidx.lifecycle.LifecycleRegistry.sync (LifecycleRegistry.java:307)
  at androidx.lifecycle.LifecycleRegistry.moveToState (LifecycleRegistry.java:148)
  at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent (LifecycleRegistry.java:134)
  at androidx.fragment.app.FragmentViewLifecycleOwner.handleLifecycleEvent (FragmentViewLifecycleOwner.java:88)
  at androidx.fragment.app.Fragment.performStart (Fragment.java:3028)
  at androidx.fragment.app.FragmentStateManager.start (FragmentStateManager.java:589)
  at androidx.fragment.app.FragmentStateManager.moveToExpectedState (FragmentStateManager.java:300)
  at androidx.fragment.app.FragmentManager.executeOpsTogether (FragmentManager.java:2189)
  at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute (FragmentManager.java:2106)
  at androidx.fragment.app.FragmentManager.execPendingActions (FragmentManager.java:2002)
  at androidx.fragment.app.FragmentManager$5.run (FragmentManager.java:524)
  at android.os.Handler.handleCallback (Handler.java:938)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:246)
  at android.app.ActivityThread.main (ActivityThread.java:8587)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:602)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1130)

The next thing, I share that string to C and edit there:

...
// Let's say again, this is executed in some onClickListener {}
share.share(sharedString)
findNavController().navigate(R.id.action_nav_b_to_nav_c)
...

And in C I get it like this:

private val share: Share by activityViewModels()

...

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    ...
    val string  share.shared.value!! // As you can see without calling observe.

    // Now we change the value and send it back
    share.share("My new string for B!")
    findNavController().navigateUp()
    ...
}

Questions:

  1. What is the best approach in this case, to observe the shared data like in B or get directly over value like in C?
  2. Why does the app sometimes crash?
  3. Do I do it in wrong way or miss something?

I do not think that the Google’s approach is the best solution. I think they had to bring something different and better solution to allow data sharing between fragments before releasing the navigation component. That does not look effective to me.

Nevertheless, I am trying to get it done working without exceptions.

I searched for a solution and also, tried to find the problem with that exception, but no success yet.

Solution

As you are using jetpack navigation component, you can try passing objects/strings via nav args (navigation arguments). Personally, I feel it to be lot more easier when passing data between fragments. Article: https://medium.com/androiddevelopers/navigating-with-safeargs-bf26c17b1269

You need to add arguments to destination fragment with data type and default value. So when you navigation, you can add the argument and fetch the value from destination fragment like this,

private val dataFromArgument
        by lazy { DestinationFragmentArgs.fromBundle(requireArguments()).variableYouPassed}

Answered By – Ankush

Leave a Comment