如何使用导航架构组件从片段中获取结果?

116

假设我们有两个片段:MainFragmentSelectionFragment。第二个是用于选择某个对象,例如整数。接收来自此第二片段的结果有不同的方法,如回调、总线等。

现在,如果我们决定使用导航架构组件来导航到第二个片段,我们可以使用以下代码:

NavHostFragment.findNavController(this).navigate(R.id.action_selection, bundle)

当然,bundleBundle 的一个实例。正如您所看到的,我们无法访问 SelectionFragment,因此无法在其中放置回调函数。问题是,如何使用导航架构组件接收结果?


1
SelectionFragment直接或间接地更新共享的ViewModel,而MainFragment则订阅以了解该ViewModel中的更改。 - CommonsWare
1
只要您使用的是与导航组件无关的ViewModel - Nominalista
4
没问题。它们是设计成能够一起工作的,而谷歌表明,在使用导航库时,共享的“ViewModel”是片段之间通信的推荐方式 - CommonsWare
2
我认为你应该将其发布为答案。 - Nominalista
1
使用图形范围的共享视图模型。https://dev59.com/ElMI5IYBdhLWcg3w2_jm - Mukhtar Bimurat
显示剩余2条评论
9个回答

112

他们在2.3.0-alpha02版本中添加了一个修复此问题的

如果从Fragment A导航到Fragment B并且A需要来自B的结果:

findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(viewLifecycleOwner) {result ->
    // Do something with the result.
}

如果在片段B中需要设置结果:

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

我最终创建了两个扩展程序:

fun Fragment.getNavigationResult(key: String = "result") =
    findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)

fun Fragment.setNavigationResult(result: String, key: String = "result") {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

6
这应该被接受为正确答案。如此简单,且能完美地完成工作。 - joghm
1
java.lang.IllegalArgumentException: 无法将类型为com.experienceapi.auth.models.LogonModel的值放入保存状态。有什么想法吗? - kgandroid
从文档中:该值必须具有可以存储在android.os.Bundle中的类型。 - Valentin Yuryev
4
注意,currentBackStackEntry<dialog> 目标上无法正常工作(请参阅文档),您需要使用目标 ID 的 getBackStackEntry() - arekolek
1
它存在一个问题。如果你从Fragment B导航到Fragment A,Fragment A会从LiveData中获取数据,这是没问题的。但是,如果你重新启动该片段(配置更改),那么观察者会再次起作用,因为LiveData已经缓存了数据。 - Andrew
显示剩余9条评论

101

自从 KTX 1.3.6 片段,Android 支持在片段之间或片段和活动之间传递数据。这类似于 startActivityForResult 逻辑。

以下是使用导航组件的示例。您可以在 此处 阅读更多信息。

build.gradle

implementation "androidx.fragment:fragment-ktx:1.3.6"

FragmentA.kt

class FragmentA : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        // Step 1. Listen for fragment results
        setFragmentResultListener(FragmentB.REQUEST_KEY) { key, bundle -> 
            // read from the bundle
        }

        // Step 2. Navigate to Fragment B
        findNavController().navigate(R.id.fragmentB)
    }
}

FragmentB.kt

class FragmentB : Fragment() {

    companion object {
        val REQUEST_KEY = "FragmentB_REQUEST_KEY"
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        buttonA.setOnClickListener { view -> 
            // Step 3. Set a result
            setFragmentResult(REQUEST_KEY, bundleOf("data" to "button clicked"))
            
            // Step 4. Go back to Fragment A
            findNavController().navigateUp()
        }
    }
}    

1
你的解决方案存在问题:https://dev59.com/wL3pa4cB1Zd3GeqPWAMQ - Morteza Rastgoo
1
非常有趣和简单的决策,这正是我需要从子片段返回数据的。 - Black_Zerg
@MortezaRastgoo 我认为这个问题可以通过使用ViewModel或任何持久化数据策略来解决。 - Hanako
3
不起作用,Fragment A结果监听器从未被调用,不知道为什么。 - amir_a14
这里也不起作用。 - william xyz

43

使用这些扩展函数

fun <T> Fragment.getNavigationResult(key: String = "result") =
    findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)

fun <T> Fragment.getNavigationResultLiveData(key: String = "result") =
        findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)

fun <T> Fragment.setNavigationResult(result: T, key: String = "result") {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

所以,如果你想从片段B向片段A发送结果。
在片段B内部。
setNavigationResult(false, "someKey")

在FragmentA内部

val result = fragment.getNavigationResultLiveData<Boolean>("someKey")
result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)

重要提示

在Fragment B中,您需要在已启动或已恢复状态下(在onStop()或onDestroy()之前)设置结果(setNavigationResult()),否则previousBackStackEntry将已经为null。

重要提示 #2

如果您只想处理一次结果,则必须在SavedStateHandle上调用remove()以清除结果。如果您不删除结果,则LiveData将继续向任何新的Observer实例返回最后一个结果。

更多信息请参见官方指南


这是个很棒的答案。 - reza_khalafi
2
我尝试使用savedStateHandle并成功设置了键值,但是我的观察者从未收到更新。我正在使用2.3.4版本。 - shadygoneinsane
请注意,如果 "result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)}" 无法解析为布尔值,请将函数放在括号内,像这样:"observe(viewLifecycleOwner, Observer { booleanValue -> })"。祝编码愉快! - Hanako
如果这对你不起作用,请尝试将“scope from viewLifecycleOwner”替换为“findNavController().currentBackStackEntry”。 - Jan
如果用户再次回到第一个Fragment,这种方式会存在问题,LiveData将被调用两次或更多次,请使用此帖子https://dev59.com/Frvoa4cB1Zd3GeqP5opY#69005732。 - Rasoul Miri
显示剩余3条评论

30

根据Google:您应该尝试使用共享ViewModel。请查看以下来自Google的示例:

Shared ViewModel将包含共享数据,可以从不同的片段访问。

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

更新ViewModel的MasterFragment:

public class MasterFragment extends Fragment {

    private SharedViewModel model;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

使用共享ViewModel的DetailsFragment:

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, item -> {
           // Update the UI.
        });
    }
}

25
这种方式存在问题,你正在使用 getActivity() 来限定你的 ViewModel 的作用域。这意味着它永远不会被清除(会使用额外的内存,并且在稍后返回这些片段时可能导致意想不到的结果,显示陈旧的数据)。相反,你应该使用 ViewModelProviders.of(parent)... - Carson Holzheimer
3
如果我们没有视图模型,该怎么办? - Cyph3rCod3r
2
@CarsonHolzheimer 这里的 parent 是什么? - Anup Ammanavar
2
@CarsonHolzheimer,你所说的问题在这里得到了解决:https://dev59.com/ElMI5IYBdhLWcg3w2_jm - Mukhtar Bimurat
1
我不会说已经解决了。它们只是一些不太理想的解决方法。总的来说,共享视图模型并不总是一个好主意。对于简单地将结果返回到先前的活动,它们很难阅读,并且比即将推出的带有结果返回的API更难构建。 - Carson Holzheimer
显示剩余4条评论

6

在对@LeHaine的回答做出一点改进后,你可以使用这些方法进行导航2.3.0-alpha02及以上版本。

fun <T> Fragment.getNavigationResult(key: String) =
    findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)

fun <T> Fragment.setNavigationResult(result: T, key: String) {
    findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

1
我创建了一个类似于LeHaine的答案的包装函数,但它处理更多情况。
将结果从子组件传递给父组件:
findNavController().finishWithResult(PickIntervalResult.WEEKLY)

从父元素中获取子元素的结果:
findNavController().handleResult<PickIntervalResult>(
    viewLifecycleOwner,
    R.id.navigation_notifications, // current destination
    R.id.pickNotificationIntervalFragment // child destination
    ) { result ->
        binding.textNotifications.text = result.toString()
}

我的包装器不像LeHaine的那么简单,但它是通用的,并处理以下情况:

  1. 一个父级有几个子级
  2. 结果是任何实现Parcelable接口的类
  3. 对话框目标

请查看Github上的实现,或查看一篇解释其工作原理的文章


0
fun <Type> BaseNavigationActivity<*,*,*>.getNavigationResult(key : String, @IdRes viewId: Int)  =
    this.findNavController(viewId).currentBackStackEntry?.savedStateHandle?.getLiveData<Type>(key)


fun <Type> BaseNavigationActivity<*,*,*>.setNavigationResult(result: Type, key: String, @IdRes viewId: Int){
   this.findNavController(viewId).previousBackStackEntry?.savedStateHandle?.set<Type>(key, result)
}

5
欢迎来到 Stack Overflow。当代码配有解释时,它会更有帮助。Stack Overflow 旨在学习,而非提供盲目复制粘贴的片段。请编辑您的答案并说明如何回答具体问题。参见How to Answer - Vimal Patel

-3
我建议您使用NavigationResult库,它是JetPack Navigation组件的附加组件,可以让您使用Bundle进行navigateUp操作。我还在Medium上写了一篇博客文章介绍了这个库。

3
我不建议使用这个库,因为它高度依赖继承。迟早会在你的努力中限制你。 TL;DR 倾向于使用组合而非继承:https://medium.com/@rufuszh90/effective-java-item-16-favour-composition-over-inheritance-ed82e482fd1a - Mehlyfication

-3

这只是其他答案的替代方案...

使用MutableShareFlow作为共享对象(例如:repo)的核心EventBus,并在这里描述的观察者。

看起来事情正在远离LiveData并朝着Flow方向发展。

值得一看。


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