0

I use the navigation component to do various screen transitions.

Pass the title data from A fragment to B fragment at the same time as the screen is switched. (using safe args)

In fragment B, set the data received from A. And to keep the title data even when the screen is switched, I set it in LiveData in the ViewModel.

But if you go back from fragment B to fragment C, B's title is missing.

Some say that because this is a replace() method, a new fragment is created every time the screen is switched.

How can I keep the data even when I switch screens in the Navigation Component?

Note: All screen transitions used findNavController.navigate()!

fragment A

startBtn?.setOnClickListener { v ->
        title = BodyPartCustomView.getTitle()
        action = BodyPartDialogFragmentDirections.actionBodyPartDialogToWrite(title)
        findNavController()?.navigate(action)
}

fragment B

class WriteRoutineFragment : Fragment() {

    private var _binding: FragmentWritingRoutineBinding? = null
    private val binding get() = _binding!!
    private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
    private val args : WriteRoutineFragmentArgs by navArgs() // When the screen changes, it is changed to the default value set in <argument> of nav_graph

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

        viewModel.setValue(args) // set Data to LiveData
        viewModel.title.observe(viewLifecycleOwner) { titleData ->
            // UI UPDATE
            binding.title.text = titleData
        }
    }

UPDATED Navigation Graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/calendar">
    
    <!--  fragment A  -->
    <dialog
        android:id="@+id/bodyPartDialog"
        android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
        android:label="BodyPartDialogFragment"
        tools:layout="@layout/fragment_body_part_dialog">
        <action
            android:id="@+id/action_bodyPartDialog_to_write"
            app:destination="@id/write"/>
    </dialog>

    <!-- fragment B   -->
    <fragment
        android:id="@+id/write"
        android:name="com.example.writeweight.fragment.WriteRoutineFragment"
        android:label="WritingRoutineFragment"
        tools:layout="@layout/fragment_writing_routine">

        <action
            android:id="@+id/action_write_to_workoutListTabFragment"
            app:destination="@id/workoutListTabFragment" />
        <argument
            android:name="title"
            app:argType="string"
            android:defaultValue="Unknown Title" />
    </fragment>
    <!-- fragment C   -->
    <fragment
        android:id="@+id/workoutListTabFragment"
        android:name="com.example.writeweight.fragment.WorkoutListTabFragment"
        android:label="fragment_workout_list_tab"
        tools:layout="@layout/fragment_workout_list_tab" >
        <action
            android:id="@+id/action_workoutListTabFragment_to_write"
            app:destination="@id/write"
            app:popUpTo="@id/write"
            app:popUpToInclusive="true"/>
    </fragment>
</navigation>

UPDATED ViewModel( This is the view model for the B fragment.)

class WriteRoutineViewModel : ViewModel() {
    private var _title: MutableLiveData<String> = MutableLiveData()

    val title: LiveData<String> = _title

    fun setValue(_data: WritingRoutineFragmentArgs) {
        _title.value = _data.title
    }
}

Error

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.writeweight, PID: 25505
    java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
     Caused by: java.lang.reflect.InvocationTargetException
        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) 
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:52)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
        at com.example.writeweight.fragment.WriteRoutineFragment.getArgs(Unknown Source:4)
        at com.example.writeweight.fragment.WriteRoutineFragment.onViewCreated(WriteRoutineFragment.kt:58)
        at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
        at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
        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:8512)
        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) 
     Caused by: java.lang.IllegalArgumentException: Required argument "title" is missing and does not have an android:defaultValue
        at com.example.writeweight.fragment.WriteRoutineFragmentArgs$Companion.fromBundle(WriteRoutineFragmentArgs.kt:26)
        at com.example.writeweight.fragment.WriteRoutineFragmentArgs.fromBundle(Unknown Source:2)
ybybyb
  • 1,385
  • 1
  • 12
  • 33
  • 1
    Looks to me like the problem lies in `viewModel.setValue(args)`. What does this function do if the args value for the title is missing, as it may be when you navigate back to B? – Tenfour04 Jul 08 '21 at 19:42
  • It seems to be set to the default value I set for `nav_graph`, as I expected. In fact, it was set to the default value. Updated `nav_graph`. But how do I set a value in `LiveData` without `viewModel.setValue`? – ybybyb Jul 08 '21 at 19:47
  • 1
    I would make the title argument nullable and pass null as the default value. Then in `viewModel.setValue()`, ignore it if it's null instead of passing it to the LiveData. – Tenfour04 Jul 08 '21 at 19:53
  • 1
    Possible duplicate of https://stackoverflow.com/questions/59232880/my-fragments-keep-recreating-whenever-i-reclick-or-navigate-to-the-next-fragment/59236146#59236146 – Syed Ahmed Jamil Jul 08 '21 at 19:54
  • @Tenfour04 I set the title argument of `NavGraph` to `nullable` and checked for null in `setValue()`, but when I switch to `C->B`, I still get an error – ybybyb Jul 08 '21 at 20:09
  • 1
    I would need to know what the error is and see your `setValue()` code to have any idea what's going wrong. – Tenfour04 Jul 08 '21 at 21:11
  • @Tenfour04 I added the code for the ViewModel, and also the error code. – ybybyb Jul 09 '21 at 13:02

2 Answers2

2

A single ViewModel can also be used for multiple fragments. Fragments are obviously showing inside an activity. The ViewModel in the activity can be passed to each fragment, that has the reference from the title. It is the solution if you want to solve using ViewModel.

Otherwise you can try the savedInstance method for solving this issue. Here is a thread about it.

Eishon
  • 1,274
  • 1
  • 9
  • 17
  • you mean [sharedviewmodel](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#sharing)? – ybybyb Jul 09 '21 at 13:05
  • 1
    The OP is already using a shared view model in their code (`by activityViewModels()`). – Tenfour04 Jul 09 '21 at 13:18
  • @Tenfour04 That's the way I tried it too. As for the result, it was successful, but when using the `shared view model` in the `MVVM` pattern, I did not know how to handle the `view model` for each fragment (`A`,`B`), so I changed the method. – ybybyb Jul 09 '21 at 17:10
1

Following from my comment:

I would make the title argument nullable and pass null as the default value. Then in viewModel.setValue(), ignore it if it's null instead of passing it to the LiveData.

The ViewModel's setValue() function should look like:

fun setValue(_data: WritingRoutineFragmentArgs) {
    _data.title?.let { _title.value = it }
}

so the value is only passed along to the LiveData if it is not the default (null).

Your xml for the value should mark the default as @null and needs to have nullable="true". Your stack trace looks like there was a problem with how you specified the default or making it nullable.

<argument
    android:name="title"
    app:argType="string"
    app:nullable="true"
    android:defaultValue="@null" />

IMO, for proper separation of concerns, the ViewModel should not have any awareness of navigation arguments. The setValue function should take a String parameter, and you should decide in the fragment whether to update the ViewModel. Like this:

// In ViewModel
fun setNewTitle(title: String) {
    _title.value = title
}

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

    args.title?.let { viewModel.setNewTitle(it) } // set Data to LiveData

    viewModel.title.observe(viewLifecycleOwner) { titleData ->
        // UI UPDATE
        binding.title.text = titleData
    }
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks. I set the default to `@null` and no more errors. However, the title value is still not maintained in the `B->C->B` transition. In my opinion, isn't it a problem that the value of the property `args` of `fragment B` is set to the `default value` as the screen is switched? – ybybyb Jul 09 '21 at 17:00
  • 1
    It should be fine because the default value is now null, and we use `?.let` to ignore it if it is null. I see that you edited your question to use `by viewModels()` instead of `by activityViewModels()`. This would cause your ViewModel to no longer be shared by different fragments. I think this could be what's causing the value to change back to the default. You should use `activityViewModels()` so it is shared and persistent. – Tenfour04 Jul 09 '21 at 18:31
  • Actually, `activityViewModels` is just a mistake code I wrote when trying to use `sharedVieModel`. I also confirmed that the title is maintained by using the same view model as fragment `A` and `B` when using `acitityViewModels`. By the way, the OP's `activityviewmodel` should be the original `viewModels { }`. That is the intent of the question. Using `viewmodels` doesn't work the behavior I want? – ybybyb Jul 09 '21 at 18:44
  • 1
    I think it's possible `viewModels` won't work, depending on your navigation path, like if you do something that causes the second B fragment to be a new instance of B than the first B fragment. If you use `activityViewModels`, you know for sure that you only ever have one instance of the ViewModel throughout all the fragments of your Activity. – Tenfour04 Jul 09 '21 at 18:48
  • It's a little hard to understand. I used `activityViewModel` as seen [here](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#sharing). Documents use `one viewmodel` in two fragments. That is, both fragments share the same `viewmodel`, passing data through it. The documentation tells us to use `activityViewModels` for `SharedViewModels`. What does this mean? Actually I don't know the difference between `activityViewModels` and `viewModels`. – ybybyb Jul 09 '21 at 19:07
  • In the document, `two fragments` use `one viewmodel` and use `activityViewModels`, but I used` activityViewModels` with `one viewmodel` in `one fragment`. Data passing was using `safeArgs`. And I finally solved the problem. But I don't know what `activityViewModels` is and why it's resolved. You mentioned that using viewModels is not shared across other fragments. But as I said, I pass the data as `safeargs` and use the `viewmodel` only in the `B fragment`. What does that have to do with it? – ybybyb Jul 09 '21 at 19:17
  • 1
    If the way you are navigating through fragments results in a second instance of B, each instance will have a separate instance of the ViewModel if you use `by viewModels`, but they will share the same instance if you use `by activityViewModels`. The word "activity" in `activityViewModels` signifies that the ViewModel it retrieves is scoped to the host Activity instead of the individual Fragment. – Tenfour04 Jul 09 '21 at 19:46
  • good. I think I understand a little bit. But from your explanation, I think it's like a `static member` of `java` or a `companion object` of `kotlin`. In other words, there are several `fragments` created no matter how many times the screen is switched, but in `viewmodels`, a `ViewModel` is created for each `fragment`, and `activityViewModels` means that all `fragments` using this use the same `viewmodel`? – ybybyb Jul 09 '21 at 20:06
  • 1
    Yes, Jetpack keeps a singleton registry of all ViewModel instances and automatically removes them when the object they are scoped to reaches end of life. When you retrieve a view model, first it checks if there is already an instance that is in scope. If your scope is only the fragment, it will only give you the same instance if this is the same fragment instance in the navigation tree. But if the scope is the Activity, you will always get the same instance. – Tenfour04 Jul 09 '21 at 20:37
  • Oh, thanks, I can finally proceed to the next code. – ybybyb Jul 10 '21 at 17:48