如何在使用导航组件时设置对话框的目标片段

20

我正在使用childFragmentManager在Fragment中显示对话框,或者在Activity中使用supportFragmentManager。我想设置目标Fragment,如下所示:

val textSearchDialog = TextSearchDialogFragment.newInstance()
textSearchDialog.setTargetFragment(PlaceSearchFragment@this, 0)

但是当运行该代码时,我遇到了错误:

java.lang.IllegalStateException: 片段 TextSearchDialogFragment{b7fce67 #0 0} 声明的目标片段 PlaceSearchFragment{f87414 #0 id=0x7f080078} 不属于 此片段管理器!

我不知道如何访问导航组件正在使用的FragmentManager来管理显示该片段,有解决方案吗?


似乎问题在于,您必须使用相同的片段管理器而不是子级,请使用getFragmentManager()。 - Pavneet_Singh
4个回答

18
更新: 作为 Navigation 2.3.0 的一部分,Navigation 显式地支持使用 返回结果 并提供了一个特定的部分用于 从对话框目的地返回结果 ,这是使用共享 ViewModel 的替代方案。

先前的回答:

与 Navigation Architecture 组件通信的推荐模式是通过 共享的ViewModel 实现的 - 通过使用 ViewModelProvider(getActivity()) 检索 ViewModel 来实现在 Activity 级别保持 ViewModel

根据文档,这提供了许多好处:

  • Activity 不需要做任何事情,或者知道这种通信的任何内容。
  • 除了 SharedViewModel 契约之外,Fragment 不需要知道彼此的任何信息。如果其中一个 Fragment 消失,则另一个仍然像往常一样工作。
  • 每个片段都有自己的生命周期,并且不受其他片段生命周期的影响。如果一个片段替换了另一个片段,则UI仍然可以正常工作。

您还可以使用导航图范围的ViewModel在较小的范围内共享ViewModel,而不是整个活动。


11
我认为这是错误的答案。你不可能想创建一个共享的 ViewModel,让它在父 Activity 的整个生命周期内存在,只是为了与你的对话框进行通信。实际上,导航框架缺乏添加目标片段的功能。 - konata
9
即使Google推荐使用Shared ViewModel,但这个名称听起来不太对。它看起来类似于通过全局变量从函数返回值。当我向详细/编辑片段前进时,使用newFragment.setTargetFragment(this)可以实现预期的流程,然后在返回之前调用getTargetFragment().onActivityResult(result, getTargetRequestCode())(例如fragmentManger.popBackstack())。其中一个好处是Android自动处理片段不存在的情况。但是,我还没有找到使用Navigation Component实现这种流程的方法。 - jskierbi
5
当然,那个问题有相当大的问题——你的目标Fragment尚未启动,因此在那里执行任何操作都是不安全的。你把两个Fragment紧密耦合在一起,而且你被迫使用Intent(和Parcelable对象),实际上任何对象都可以做到这一点。当然,它并不完美,所以我建议为现有功能请求打星,以获得本地navigateForResult()类型的API。 - ianhanniballake
1
@ianhanniballake 当这两个共享片段都被销毁时,这个 sharedViewModel 会发生什么?它会被垃圾回收还是一直留在内存中直到活动结束? - rd7773
如果您使用getActivity()或requireActivity(),所有数据将在Activity完成之前保留,这意味着弹出片段将保留共享视图模型中的数据(如果您没有在弹出片段的onDestroy()中更新它),因为该实例位于Activity的生命周期内,而不是Fragment的生命周期内。@rd7773 - Gastón Saillén
显示剩余3条评论

6

对于已接受的答案进行详细阐述:

(1) 创建一个共享视图模型,用于在该活动中的片段之间共享数据。

public class SharedViewModel extends ViewModel {

    private final MutableLiveData<Double> aDouble = new MutableLiveData<>();

    public void setDouble(Double aDouble) {
        this.aDouble.setValue(aDouble);
    }

    public LiveData<Double> getDouble() {
        return aDouble;
    }
}

(2) 将您想要访问的数据存储在视图模型中。注意视图模型的作用域 (getActivity)。

SharedViewModel svm =ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
svm.setDouble(someDouble);

(3) 让该片段实现对话框的回调接口,并在不设置目标片段的情况下加载对话框。

fragment.setOnDialogSubmitListener(this);
fragment.show(getActivity().getSupportFragmentManager(), TAG);

(4) 在对话框内检索数据。

SharedViewModel svm =ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
svm.getDouble().observe(this, new Observer<Double>() {
    @Override
    public void onChanged(Double aDouble) {
        // do what ever with aDouble
    }
}); 

4
使用viewmodel和fragment ktx,您可以在父片段和子片段之间托管共享的viewmodel,因此,不必让activity包含viewmodel实例并存储数据,直到该activity完成,而是将viewmodel存储在父片段中。这样做,当弹出实例化viewmodel的片段时,viewmodel将被清除。

导入

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
 implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'

ParentFragment(SharedViewModel宿主)

class ParentFragment:Fragment() {
 private val model: SharedViewModel by viewModels()
}

子碎片

class ChildFragment:Fragment(){
private val model: SharedViewModel by viewModels ({requireParentFragment()})
}

这样做将把sharedviewmodel托管在父片段中,依赖于该父片段的子片段将可以访问该SharedViewModel的相同实例,并且当您弹出(即销毁片段)时,您的onCleared()方法将在您的视图模型中触发,这个shareviewmodel将被清除,以及所有它的数据。
这样,您不需要让MainActivity包含所有片段共享的数据,也不需要在每次离开使用SharedViewModel的片段时清除该数据。
现在在alpha版中,您可以使用一个视图模型在导航之间传递数据,在Fragment B和片段A之间共享数据,现在只需使用两行即可。

https://developer.android.com/guide/navigation/navigation-programmatic#returning_a_result


2
比被接受的答案好多了。如果您想在两个片段之间“一次”共享数据,那么作用域为活动的SharedViewModel绝对不是正确的方法。 - reVerse

0

现有的回答都没有回答你的问题 - 当使用导航组件时,我们如何设置对话框的目标片段?

事实证明,在使用导航组件时,我们不需要使用共享ViewModel的(可怕)模式。一旦你知道了如何做,实际上非常容易设置对话框的目标片段。

我已经写了一篇关于它的完整文章,你可以在这里阅读:

https://lukeneedham.medium.com/using-targetfragment-with-jetpack-navigation-component-9c4302e8c062

您也可以在此处查看Gist:

https://gist.github.com/LukeNeedham/83f0bdaa8d56d03d11f727967eb327f2

关键是自定义的 FragmentFactory:
fun FragmentManager.autoTarget() {
    fragmentFactory = ChildManagerFragmentFactory(this)
}

class ChildManagerFragmentFactory(
    private val fragmentManager: FragmentManager
) : AutoTargetFragmentFactory() {
    override fun getCurrentFragment() =
        fragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.firstOrNull()
}

abstract class AutoTargetFragmentFactory : FragmentFactory() {
    abstract fun getCurrentFragment(): Fragment?

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val fragment = super.instantiate(classLoader, className)
        val currentFragment = getCurrentFragment()
        fragment.setTargetFragment(currentFragment, REQUEST_CODE)
        return fragment
    }

    companion object {
        const val REQUEST_CODE = 0
    }
}

然后就可以像这样简单地使用:

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager.autoTarget()
    }
}

1
请注意,由于FragmentManager更新的批处理特性,此方法基本上是有缺陷的。例如,使用popUpTo操作将导致getCurrentFragment()返回即将弹出的片段,从而导致新片段保留对实际上将在新片段经历任何生命周期转换之前被销毁的片段的硬引用,从而立即创建内存泄漏。 - ianhanniballake
请注意,setTargetFragment()本身已被弃用,在导航层和片段层都有替代方案,与setTargetFragment()不同的是,这些替代方案将在使用即将推出的功能(如多个返回堆栈和单一生命周期)时起作用。 - ianhanniballake
嗨Ian,我只想说我是你的超级粉丝!感谢你指出了我忽略的泄漏问题。我必须说,我觉得这些替代方案有点倒退:我使用Jetpack Navigation组件,所以我不再需要担心传递参数的束缚和键,但显然当返回结果时我仍然需要关注它们。targetFragment的巨大优势在于它允许我们实现接口,这些接口可以用于以类型安全的方式传递结果,而不像基于Bundle的替代方案。 - Luke Needham
你们有没有计划实现一个类似 safe-args 的、类型安全的封装器以便以与当前传递参数方式相同的方式传递结果 @ianhanniballake? - Luke Needham

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