使用Android Architecture Navigation实现startActivityForResult()的等效方法

60

我有一个包含3个屏幕的工作流程。从“屏幕1”进入“屏幕2”,用户必须接受我在图片中称之为“模态框”的某种条款和条件。但他只需要接受这些条件一次。下一次当他在第一个屏幕上时,他可以直接进入屏幕2。用户可以选择不接受条款,因此我们返回到“屏幕1”,并且不尝试进入“屏幕2”。

App workflow

我想知道如何使用新的导航组件实现它。
以前,我会这样做:
1. 在屏幕1上,检查用户是否必须接受条件。 2. 如果没有,启动“屏幕2”活动。 3. 如果是,使用startActivityForResult()并等待模态框的结果。标记条款为已接受。启动“屏幕2”。
但是使用导航图后,无法启动片段以获取结果。
我可以在“模态”屏幕中标记条款为已接受,并从那里启动“屏幕2”。问题是,要访问屏幕2,我需要进行网络请求。我不想在“屏幕1”和“模态”中重复调用API并处理其结果。 有没有一种方法可以使用Jetpack导航从“模态”返回到“屏幕1”并携带一些信息(用户接受了条款)? 编辑:目前,我通过使用Yahya下面建议的相同流程来解决它:仅使用模态框的Activity并从“屏幕1”使用startActivityForResult。我只是想知道是否可以继续在整个流程中使用导航图。

1
一年后,有正确的实现方法吗?我尝试了被接受的答案,但没有这样的方法,API改变了吗?序列化回调是非常糟糕的解决方案。 - Pavel Poley
6个回答

23
在AndroidX Fragment库的1.3.0-alpha04版本中,他们引入了新的API,允许在Fragment之间传递数据。
引入了对通过FragmentManager上的新API在两个Fragment之间传递结果的支持。这适用于层级Fragment(父/子),DialogFragments和Navigation中的片段,并确保结果仅在Fragment至少为STARTED时发送到您的Fragment。(b/149787344FragmentManager增加了两种新方法:
- FragmentManager#setFragmentResult(String, Bundle) ,您可以将其视为现有Activity#setResult; - FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener),它允许您监听/观察结果更改。
如何使用?
FragmentA中,在onCreate方法中向FragmentManager添加FragmentResultListener
setFragmentResultListener("request_key") { requestKey: String, bundle: Bundle ->
    val result = bundle.getString("your_data_key")
    // do something with the result
}

FragmentB中添加以下代码以返回结果:
val result = Bundle().apply {
    putString("your_data_key", "Hello!")
}
setFragmentResult("request_key", result)

开始使用FragmentB,例如:通过以下方式:

findNavController().navigate(NavGraphDirections.yourActionToFragmentB())

要关闭/完成FragmentB,请调用:

findNavController().navigateUp()

现在,应该通知您的{{FragmentResultListener}}并接收结果。
(我正在使用{{fragment-ktx}}简化上面的代码)

为什么我在FragmentManager中不再看到setFragmentResultListener,但是它在文档中存在:https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentManager#setfragmentresultlistener?我已经尝试了v2.2.2和v2.3.0-rc01。 - android developer
1
@androiddeveloper setFragmentResultListener 存在于 fragment-ktx 组件中。如果您没有使用它,可以尝试在您的 Fragment 中调用 fragmentManager.setFragmentResultListener() - Ziem
Fragment.setFragmentResultListener 的文档说明如下:一旦此 Fragment 至少处于 androidx.lifecycle.Lifecycle.State.STARTED 状态,使用相同 requestKey 设置的任何结果都将传递到 FragmentResultListener.onFragmentResult 回调函数。我可以确认这种行为。 - Ziem
但如果当前 Fragment 是第二个,它怎么可能处于“STARTED”状态呢?例如尝试设置该值,但延迟调用 findNavController().navigateUp()(或者干脆不调用)并使用日志打印回调被调用的时间。 - android developer
当我返回到第一个“Fragment”并且在该第一个“Fragment”的“onStart”之后,它会触发“onFragmentResult”回调。您从“FragmentA”开始->“FragmentB”->“setFragmentResult”->您按返回键->关闭“FragmentB”->调用“FragmentA”的“onStart”->然后触发“onFragmentResult”。 - Ziem
显示剩余2条评论

22

共享视图模型有一些替代方案。

  1. 按照此处的说明使用fun navigateBackWithResult(result: Bundle)

  2. 创建一个回调函数。

ResultCallback.kt

interface ResultCallback : Serializable {
    fun setResult(result: Result)
}
将此回调函数作为参数传递(注意它必须实现Serializable并且接口需要在其自己的文件中声明。)
<argument android:name="callback"
                  app:argType="com.yourpackage.to.ResultCallback"/>

让片段A实现ResultCallback,然后通过args.callback.setResult(x)将数据传回给调用它的片段B。


谢谢!那篇文章真是非常宝贵。 - Andre Romano
感谢您提供如此精彩的参考! - Adam
这个解决方案对我不起作用。当我从Fragment A(调用者)导航到Fragment B(接收者)时,Fragment A实例被销毁了。Fragment B引用了一个旧的已死的Fragment A。 - Pablo Valdes
它可以工作,但这是一个好的实践吗?我没有找到任何关于此的谷歌建议。 - G_comp
2
这对我来说一直很好用,我认为这是最好的解决方案,直到应用程序在目标片段上被置于后台时崩溃并显示“NotSerializableException”。 - Pilot_51
使用Serializable回调方法时必须非常小心 - 非常容易意外地使您的回调不可序列化,并且只有在运行时重新创建组件时才会发出警报。这在此处详细说明:https://medium.com/@lukeneedham/listeners-in-dialogfragments-be636bd7f480 - Luke Needham

20

目前看来,导航组件中没有startActivityForResult的等效功能。但是,如果您正在使用LiveData和ViewModel,则可能会对这篇文章感兴趣。作者使用活动范围的ViewModel和LiveData来为片段实现此功能。


5
好的,这是需要翻译的内容:"Right. It was confirmed by Google in this bug https://issuetracker.google.com/issues/79672220"。 - Jonas Schmid

19
最近(在androidx-navigation-2.3.0-alpha02中),Google发布了一种使用Fragment实现此行为的正确方法。
简而言之:(来自发布说明)
如果Fragment A需要从Fragment B获取结果…
A应该从currentBackStackEntry获取savedStateHandle,调用getLiveData提供一个key并观察结果。
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(
    viewLifecycleOwner) {result ->
    // Do something with the result.
}

B 应该从 previousBackStackEntry 中获取 savedStateHandle,并使用与 A 中 LiveData 相同的 key 设置结果。

findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)

相关文档


我在Fragment A的观察器中的TextView上得到了null。 - Sagar Maiyad
3
еҰӮжһңжҲ‘жңүдёҖдёӘйңҖиҰҒд»ҺActivity BиҺ·еҸ–з»“жһңзҡ„Activity AпјҢиҝҷдјҡжҖҺд№Ҳж ·пјҹжҲ‘зӣёдҝЎиҝҷдёҚдјҡиө·дҪңз”ЁпјҢеӣ дёәдёӨдёӘжҙ»еҠЁдёӯзҡ„navControllerжҳҜдёҚеҗҢзҡ„гҖӮ - David Seroussi
1
David,对于这个活动,我们将使用老式的 startActivityForResult 机制。 - Juis Kel
1
这还能用吗?我无法访问“currentBackStackEntry”和“savedStateHandle”。 - Pavel Poley
在onStartActivityForResult中,我们有一个请求ID。这里有什么?看起来似乎无法通过这种方式区分片段请求。而“Type”是键值的类型,对吗?但如果我们处理多个为结果启动的片段怎么办?有没有使用此代码的工作Github示例?此外,我尝试使用它,似乎回调被调用两次:一次在设置时,另一次在返回第一个片段时。为什么会这样? - android developer
对我来说可以运行,但别忘了在设置结果后导航回去。findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result) findNavController().navigateUp() - ilham suaib

1
这里有另一种替代方法。您可以使用模态框返回到屏幕1的另一个导航操作,而不是使用popBackStack()。在该操作中,您可以将任何您想要的数据发送到屏幕1。使用此策略,确保模态屏幕不会保留在导航回退堆栈中: https://dev59.com/X63la4cB1Zd3GeqPJ0BD#54015319
我唯一看到的问题是,按下返回按钮将不会发送任何数据回去,但是大多数用例都需要在特定用户操作后进行导航,并且在这些情况下,这种解决方法将起作用。

0
要获取调用者片段,可以使用类似于fragmentManager.putFragment(args, TargetFragment.EXTRA_CALLER, this)的方法,然后在目标片段中使用以下代码获取调用者片段:
if (args.containsKey(EXTRA_CALLER)) {
    caller = fragmentManager?.getFragment(args, EXTRA_CALLER)
    if (caller != null) {
        if (caller is ResultCallback) {
            this.callback = caller
        }
    }
}

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