ybybyb
ybybyb

Reputation: 1729

How can I keep data when fragment is replaced?

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)

Upvotes: 0

Views: 1569

Answers (2)

Tenfour04
Tenfour04

Reputation: 93511

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
    }
}

Upvotes: 1

Eishon
Eishon

Reputation: 1324

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.

Upvotes: 2

Related Questions