替换片段时如何保留数据?

How can I keep data when fragment is replaced?

我使用 navigation component 进行各种屏幕转换。

在画面切换的同时将title dataA fragment传递给B fragment。 (使用 safe args

在片段B中,设置从A接收到的数据。 为了保持title data即使在屏幕切换时,我将它设置在LiveDataViewModel

但是如果你从 fragment B 回到 fragment CB 缺少标题。

有人说因为这是replace()方法,所以每次切换画面都会新建一个fragment

如何在导航组件中切换屏幕时保留数据?

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

片段A

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

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

更新导航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>

更新视图模型( 这是 B 片段的视图模型。)

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

    val title: LiveData<String> = _title

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

错误

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.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)

单个 ViewModel 也可以用于多个片段。碎片显然显示在 activity 内。 activity 中的 ViewModel 可以传递给每个具有标题引用的片段。如果你想用ViewModel来解决,这是解决方案。

否则你可以试试savedInstance方法来解决这个问题。这是关于它的thread

根据我的评论:

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.

ViewModel 的 setValue() 函数应如下所示:

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

因此,如果值不是默认值(null),则该值只会传递给 LiveData。

您的 xml 值应将默认值标记为 @null 并且需要 nullable="true"。您的堆栈跟踪看起来像您指定默认值或使其可为空的方式有问题。

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

IMO,为了正确分离关注点,ViewModel 不应该知道导航参数。 setValue 函数应该带一个字符串参数,你应该在片段中决定是否更新 ViewModel。像这样:

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