【问题标题】:How can I keep data when fragment is replaced?替换片段时如何保留数据?
【发布时间】:2021-07-10 00:48:17
【问题描述】:

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

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

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

但是如果你从fragment B回到fragment CB 的标题不见了。

有人说因为这是replace()的方法,所以每次切换屏幕都会创建一个新的fragment。

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

注意:所有屏幕转换都使用 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
        }
    }

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

更新的 ViewModel( 这是 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$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)

【问题讨论】:

  • 在我看来问题出在viewModel.setValue(args)。如果标题的 args 值丢失,这个函数会做什么?当您导航回 B 时可能会出现这种情况?
  • 它似乎设置为我为nav_graph 设置的默认值,正如我所料。事实上,它被设置为默认值。更新nav_graph。但是如何在没有viewModel.setValue 的情况下在LiveData 中设置值?
  • 我会让 title 参数为 null 并传递 null 作为默认值。然后在viewModel.setValue() 中,如果它为 null 则忽略它,而不是将其传递给 LiveData。
  • 我需要知道错误是什么并查看您的setValue() 代码以了解发生了什么问题。

标签: android kotlin android-fragments android-navigation


【解决方案1】:

单个 ViewModel 也可以用于多个片段。片段显然显示在活动中。活动中的 ViewModel 可以传递给每个片段,每个片段都有来自标题的引用。如果您想使用 ViewModel 解决,它就是解决方案。

否则您可以尝试使用 savedInstance 方法来解决此问题。这是关于它的thread

【讨论】:

  • 你的意思是sharedviewmodel
  • OP 已经在他们的代码中使用了共享视图模型 (by activityViewModels())。
  • @Tenfour04 我也是这样尝试的。至于结果,是成功了,但是在MVVM模式中使用shared view model时,不知道如何处理每个片段的view modelA,B),所以我改了方法。
【解决方案2】:

根据我的评论:

我会让 title 参数为 null 并将 null 作为默认值传递。然后在viewModel.setValue() 中,如果它为空则忽略它,而不是将其传递给 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
    }
}

【讨论】:

  • 谢谢。我将默认设置为@null 并且没有更多错误。但是,在B-&gt;C-&gt;B 转换中仍然没有维护标题值。在我看来,fragment B的属性args的值在屏幕切换时设置为default value不是问题吗?
  • 应该没问题,因为现在默认值为null,如果为null,我们使用?.let忽略它。我看到您编辑了您的问题以使用by viewModels() 而不是by activityViewModels()。这将导致您的 ViewModel 不再由不同的片段共享。我认为这可能是导致该值变回默认值的原因。您应该使用activityViewModels(),以便共享和持久化。
  • 我认为 viewModels 可能不起作用,具体取决于您的导航路径,例如,如果您执行的操作导致第二个 B 片段成为 B 的新实例,而不是第一个 B 片段。如果您使用activityViewModels,您肯定知道在您的 Activity 的所有片段中只有一个 ViewModel 实例。
  • 如果您在 Fragment 中导航的方式导致 B 的第二个实例,如果您使用 by viewModels,每个实例将有一个单独的 ViewModel 实例,但如果您使用它们将共享同一个实例使用by activityViewModelsactivityViewModels 中的“活动”一词表示它检索到的 ViewModel 的作用域是宿主 Activity 而不是单个 Fragment。
  • 是的,Jetpack 保留所有 ViewModel 实例的单例注册表,并在它们的作用域达到生命周期的对象时自动删除它们。当您检索视图模型时,首先它会检查范围内是否已有实例。如果您的作用域只是片段,那么如果这是导航树中的相同片段实例,它只会为您提供相同的实例。但是如果作用域是Activity,你总是会得到同一个实例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多