Reputation: 1729
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
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
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