如何将视图模型限定在父片段中?

27
所以我正在使用新的导航组件(具有单个活动原则),并使用共享视图模型在每个片段之间进行通信,但是,我已经到了一个点,有时需要清除视图模型,但我找不到一个好的地方来清除它。但说实话,我认为与其尝试自己清除它,我真的应该让框架为我执行,但它没有这样做,因为视图模型是共享的并且作用域限定为活动,但我认为我可以将它们的作用域限定为父片段,我画了一幅图来说明我想做的事情。my navigation flow 所以我只想在从“Child 1 Child a”返回时清除这两个视图模型,当前视图模型永远不会被清除,通过在片段中调用“this”和在子项中调用getParentFragment来获取当前视图模型无效,请问有人能提供一个例子吗? 编辑 看起来我已经在做类似的事情,但在我的情况下不起作用,所以我将添加一些代码,以下是我如何访问父片段中的第一个视图模型。
model = ViewModelProviders.of(this).get(RequestViewModel.class);

然后在子片段中,我正在进行这个操作

requestViewModel = ViewModelProviders.of(getParentFragment()).get(RequestViewModel.class);

但它没有在它们之间保留数据,它们都连接了观察者


1
您可以参考此答案 https://dev59.com/aVYO5IYBdhLWcg3wI-Zw#52732831,如果有任何疑问,请告知。 - Jeel Vankhede
好的,这就是我已经尝试过的,让我添加一些代码。 - martinseal1987
1
如果您想共享Viewmodel,则传递的对象引用必须是相同的。我认为您需要确保将相同的实例传递到.of(...)调用中。可以进行调试,或修改toString方法以返回唯一值,并记录在父Fragment和子Fragment内部传递给ViewModelProviders.of(...)的值。 - cincy_anddeveloper
@WadeWilson,我按照你的建议去做了,得到了这些RaiseRequestFragment{9afdd34 #1 id=0x7f0a0133} NavHostFragment{1333143 #0 id=0x7f0a0133},所以这些ID是相同的。 - martinseal1987
1
你可以尝试在父Fragment和子Fragment中打印或调试共享的ViewModels的哈希码,以验证它们是否被共享。如果它们是相同的,那么它们就是被共享的。同时,还要检查子Fragment的子Fragment,这可能是原因。 - Jeel Vankhede
显示剩余11条评论
9个回答

52

在您的应用程序中使用Fragment-ktx库,您可以按如下方式获取viewModel 在您的应用程序-> build.gradle

 implementation 'androidx.fragment:fragment-ktx:1.1.0'

// 获取ParentFragment中的ViewModel示例,代码如下:

 private val viewModel: DemoViewModel by viewModels()

// 在 ChildFragment 中获取与 ViewModel 相同的实例

 private val viewModel: DemoViewModel by viewModels(
    ownerProducer = { requireParentFragment() }
)

4
目前存在一个问题,当将手柄与导航组件绑定时,需要指定默认的视图模型工厂,否则这将成为默认方法。 - martinseal1987
1
@martinseal1987 我已经根据您的要求更新了更改。viewModels导入androidx.fragment.app.viewModels,这是Fragment-ktx的一部分。谢谢。 - Alok Mishra

13

好的,那么在父元素中使用它

model = ViewModelProviders.of(this).get(RequestViewModel.class);

而这是在孩子中

requestViewModel = ViewModelProviders.of(getParentFragment()).get(RequestViewModel.class);

给我的不同哈希码但相同的ID,看起来是由于导航组件,如果我将它们都更改为getParentFragment,则可以解决问题,因此我认为该组件在此处替换了片段而不是添加它们,特别感谢@WadeWilson和@JeelVankhede。


在观察部分,我们发送什么作为生命周期所有者?我收到了Unsafe call to observe with Fragment instance as LifecycleOwner from MyFragment.onViewCreated. - Bagus Aji Santoso
使用viewLifecycleOwner,pokemonListViewModel.searchPokemon.observe(viewLifecycleOwner, Observer { pokemonList -> - martinseal1987
1
所以我正在使用作用域viewModel方法:by viewModels来惰性创建viewModels。为了在片段和它启动的底部SheetFragmentDialog之间共享相同的viewModel,我需要在片段和bottomSheetFragment中都使用by viewModels({ requireParentFragment() }) - dumbfingers

11
如果您正在使用导航组件(https://developer.android.com/guide/navigation),则需要按照以下方式获取viewModel:
在您的应用中实现fragment-ktx -> build.gradle:
implementation 'androidx.fragment:fragment-ktx:1.2.5

在父 Fragment 中:

private val viewModel by viewModels<ParentFragmentViewModel>()

在子片段中。
private val viewModel by viewModels<ParentFragmentViewModel>({requireGrandParentFragment()})

requireGrandParentFragment()是Fragment的自定义扩展:

fun Fragment.requireGrandParentFragment() = this.requireParentFragment().requireParentFragment()

你需要向上转两级才能访问父Fragment的viewModel,因为导航组件中子Fragment的第一个父级是NavHostFragment,而NavHostFragment的父级是包含viewModel的parentFragment

如果您没有使用Navigation组件,则可以在子Fragment中像这样访问它:

private val viewModel by viewModels<ParentFragmentViewModel>({requireParentFragment()})

我来晚了,但我想知道你是否能够使用fragmentscenario进行测试?我的测试失败了,因为被测试的片段不是子片段,而是直接附加到EmptyFragmentActivity以进行测试。我可以伪造两个父片段来解决问题,但肯定有更好的方法吧? - niall8s

8
因此,根据@martin提出的解决方案,即使将一个/多个片段作为子片段添加到父片段中,导航组件也会为两个片段提供相同的片段管理器

这意味着,即使片段被添加为父子层次结构,它们也会从导航组件共享相同的Fragment管理器(可能是该库中的错误?),因此,在使用getParentFragment()实例为子片段中的ViewModelProvider时,ViewModels不会被共享。


因此,共享ViewModels的一种快速解决方法是,通过以下行从片段管理器获取父片段的实例,对于父片段和子片段都是如此:

ViewModelProviders.of(getParentFragment()).get(SharedViewModel.class); // using this in both parent amd child fragment would do the trick !

1
如果您在xml中将片段声明为片段并为其提供一个xml图形,则该片段将成为父片段,这意味着我可以分离一些导航流程,但不幸的是,这有时会破坏您的popBackStack调用,因为您最终需要一个容器来容纳该片段。我讨厌导航组件!我建议不要使用它,因为我认为它已经损坏了,但我的公司希望我使用它,因为它应该是未来的默认方式。再次感谢。 - martinseal1987
是的,实际上建议如果任何库或依赖项处于 alpha 阶段,则避免或尽量不使用它,直到它稳定为止;为了最终稳定的应用程序。 - Jeel Vankhede
你可以通过调用 getActivity().getSupportFragmentManager().popBackStack() 来修复后退按钮。 - martinseal1987
1
如果它是父片段,getParentFragment() 不会返回 null。在这种情况下,该方法会抛出异常。 - Archie G. Quiñones
不,object 将只是 null。这段代码不会抛出任何异常。 - Jeel Vankhede

5

现在,Google 已经使得我们能够将 ViewModel 的范围限定在导航图中。如果您已经在使用导航组件,则可以使用它。(个人意见,如果您尚未使用导航组件,您应该迁移到它,因为它非常容易使用。请相信一个曾试图自行管理后退堆栈的人的话)

您可以选择需要在导航图中进行分组的所有片段,并执行 右键单击->移动到嵌套图->新建图

现在,这将会将所选片段移动到主导航图内的嵌套图中,如下所示:

<navigation app:startDestination="@id/homeFragment" ...>
    <fragment android:id="@+id/homeFragment" .../>
    <fragment android:id="@+id/productListFragment" .../>
    <fragment android:id="@+id/productFragment" .../>
    <fragment android:id="@+id/bargainFragment" .../>
    
    <navigation 
        android:id="@+id/checkout_graph" 
        app:startDestination="@id/cartFragment">

        <fragment android:id="@+id/orderSummaryFragment".../>
        <fragment android:id="@+id/addressFragment" .../>
        <fragment android:id="@+id/paymentFragment" .../>
        <fragment android:id="@+id/cartFragment" .../>

    </navigation>
    
</navigation>

现在,在片段中初始化ViewModel时,请执行以下操作

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

如果您需要传递ViewModel工厂(可能是为了注入ViewModel),可以按以下方式执行:

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }

请确保它是 R.id.graph 而不是 R.navigation.graph

由于某种原因,创建导航图并使用 include 将其嵌套在主导航图中对我没有起作用。可能是个错误。

来源:https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e

感谢Manually clearing an Android ViewModel? 指引我朝正确方向。


1
完全同意,我提出了另一个问题,需要相同的答案,链接在这里:https://dev59.com/DVMI5IYBdhLWcg3wUp02 - martinseal1987

4
我做错的事情是向 DialogFragment 提供了错误的片段管理器。 在我的情况下,它应该按照以下方式工作:
class MyDialog : DialogFragment() {

    private val viewModel: MyViewModel by viewModels({ requireParentFragment() })

在我的fragment中初始化对话框:

private fun showDialog(){
    MyDialog().show(childFragmentManager, "AddFriendToGroupDialog")
}

我正在使用 implementation 'androidx.fragment:fragment-ktx:1.3.0' 和导航图。


很棒,好答案,正在工作中... - Arda Kazancı

2

我遇到了同样的问题,尝试了以上所有解决方案但在我的情况下都不起作用,我想到了另一种解决方案,使用requireParentFragment()返回子片段中的NavHostFragment并解决了问题。

private val viewModel: childViewModel by viewModels(
            ownerProducer = { requireParentFragment().childFragmentManager.primaryNavigationFragment!! }
    )

在父片段中使用 this。
private val viewModel: MyOrdersVM by viewModels()

1

使用 Kotlin 和延迟初始化(又称 by viewModels)的版本

在父 Fragment 中(例如 ViewPager2)

private val viewModel: MyViewModel by viewModels({ this }) {
    TODO("MyViewModelFactory instance")
}

在子片段中(例如ViewPager2中的页面)

private val viewModel: MyViewModel by viewModels({ requireParentFragment() })

即使在ParentFragment中不传递this,它也能正常工作。在viewModels()中,默认会传递this。顺便说一句,谢谢,它运行得很好。 - Pratik Saluja

0

我也遇到了这个问题。为子片段创建了一个新的ViewModel。 问题在于getParentFragment()返回NavHostFragment而不是所需的片段。

我们需要在子片段中获取真正的父对象。

如果我们执行requireParentFragment().requireParentFragment(),那么我们就可以得到真正的父对象。

解决方案:(用于子更新)

Fragment parent = requireParentFragment().requireParentFragment();
viewModel = new ViewModelProvider(parent).get(RequestViewModel.class);

在这里找到了解决方案


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接