Fragment 间通信的新方式,这些你是否又懂了呢?

1、概述

就在 2020/05/07 号 Now in Android #17 更新了,发布 Android 的新特性,其中就包括 Fragment 间通信的新方式

通过这篇文章你将学习到以下内容,将在后文部分会给出相应的答案
一、新 Fragment 间通信的方式的使用?
二、新 Fragment 间通信的源码分析?
三、汇总 Fragment 之间的通信的方式?

2、在 Fragment 之间传递数据

Fragment 间传递数据可以通过多种方式,包括使用

一、target Fragment APIs (Fragment.setTargetFragment() Fragment.getTargetFragment())
二、ViewModel
三、Fragments’ 父容器 Activity

target Fragment APIs 已经过时了,现在鼓励使用新的 Fragment result APIs 完成 Fragment 之间传递数据,其中传递数据由 FragmentManager 处理,并且在 Fragments 设置发送数据和接受数据。

使用新的 Fragment APIs 在 两个 Fragment 之间的传递,没有任何引用,可以使用它们公共的 FragmentManager,它充当 Fragment 之间传递数据的中心存储。

接受数据

如果想在 Fragment 中接受数据,可以在 FragmentManager 中注册一个 FragmentResultListener,参数 requestKey 可以过滤掉 FragmentManager 发送的数据

FragmentManager.setFragmentResultListener(
    requestKey,
    lifecycleOwner,
    FragmentResultListener { requestKey: String, result: Bundle ->
        // Handle result
    })

参数 lifecycleOwner 可以观察生命周期,当 Fragment 的生命周期处于 STARTED 时接受数据。

如果监听 Fragment 的生命周期,您可以在接收到新数据时安全地更新 UI,因为 view 的创建(onViewCreated() 方法在 onStart() 之前被调用)。



当生命周期处于 LifecycleOwner STARTED 的状态之前,如果有多个数据传递,只会接收到最新的值:



当生命周期处于 LifecycleOwner DESTROYED 时,它将自动移除 listener,如果想手动移除 listener,需要调用 FragmentManager.setFragmentResultListener() 方法,传递空的 FragmentResultListener

在 FragmentManager 中注册 listener,依赖于 Fragment 发送返回的数据。

如果在 FragmentA 中接受 FragmentB 发送的数据,FragmentA 和 FragmentB 处于相同的层级,通过 parent FragmentManager 进行通信,FragmentA 必须使用 parent FragmentManager 注册 listener

parentFragmentManager.setFragmentResultListener(...)

如果在 FragmentA 中接受 FragmentB 发送的数据,FragmentA 是 FragmentB 的父容器, 他们通过 child FragmentManager 进行通信

childFragmentManager.setFragmentResultListener(...)

listener 必须设置的Fragment 相同的 FragmentManager。

发送数据

如果 FragmentB 发送数据给 FragmentA,需要在 FragmentA 中注册 listener,通过 parent FragmentManager 发送数据

parentFragmentManager.setFragmentResult(
    requestKey, // Same request key FragmentA used to register its listener
    bundleOf(key to value) // The data to be passed to FragmentA
)

3、测试 Fragment Results

测试 Fragment 是否成功接收或发送数据,可以使用 FragmentScenario API
接受数据

如果在 FragmentA 中注册 FragmentResultListener 接受数据,你可以模拟 parent FragmentManager 发送数据,如果在 FragmentA 中正确注册了 listener,可以用来验证 FragmentA 是否能收到数据,例如,如果在 FragmentA 中接受数据并更新 UI, 可以使用 Espresso APIs 来验证是否期望的数据

@Test
fun shouldReceiveData() {
    val scenario = FragmentScenario.launchInContainer(FragmentA::class.java)

    // Pass data using the parent fragment manager
    scenario.onFragment { fragment ->
        val data = bundleOf(KEY_DATA to "value")
        fragment.parentFragmentManager.setFragmentResult("aKey", data)
    }

    // Verify data is received, for example, by verifying it's been displayed on the UI
   onView(withId(R.id.textView)).check(matches(withText("value"))) 
}

发送数据

可以在 FragmentB 的 parent FragmentManager 上注册一个 FragmentResultListener 来测试 FragmentB 是否成功发送数据,当发送数据结束时,可以来验证这个 listener 是否能收到数据

@Test
fun shouldSendData() {
    val scenario = FragmentScenario.launchInContainer(FragmentB::class.java)

    // Register result listener
    var receivedData = ""
    scenario.onFragment { fragment ->
        fragment.parentFragmentManager.setFragmentResultListener(
            KEY,
            fragment,
            FragmentResultListener { key, result ->
                receivedData = result.getString(KEY_DATA)
            })
    }

    // Send data
    onView(withId(R.id.send_data)).perform(click())

    // Verify data was successfully sent
    assertThat(receivedData).isEqualTo("value")
}

小结

虽然使用了 Fragment result APIs,替换了过时的 Fragment target APIs,但是新的 APIs 在Bundle 作为数据传传递方面有一些限制,只能传递简单数据类型、Serializable 和 Parcelable 数据,Fragment result APIs 允许程序从崩溃中恢复数据,而且不会持有对方的引用,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题。

4、译者的思考

这是译者的一些思考,总结一下 Fragment 1.3.0-alpha04 新增加的 Fragment 间通信的 API

数据接受

FragmentManager.setFragmentResultListener(
    requestKey,
    lifecycleOwner,
    FragmentResultListener { requestKey: String, result: Bundle ->
        // Handle result
    })

数据发送

parentFragmentManager.setFragmentResult(
    requestKey, // Same request key FragmentA used to register its listener
    bundleOf(key to value) // The data to be passed to FragmentA
)

那么 Fragment 间通信的新 API 给我们带来哪些好处呢:

一、在 Fragment 之间传递数据,不会持有对方的引用
二、当生命周期处于 ON_START 时开始处理数据,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
三、当生命周期处于 ON_DESTROY 时,移除监听

我们一起来从源码的角度分析一下 Google 是如何做的。

5、源码分析

按照惯例从调用的方法来分析,数据接受时,调用了 FragmentManager 的 setFragmentResultListener 方法

private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
        new ConcurrentHashMap<>();

@Override
public final void setFragmentResultListener(@NonNull final String requestKey,
                                            @NonNull final LifecycleOwner lifecycleOwner,
                                            @Nullable final FragmentResultListener listener) {
    // mResultListeners 是 ConcurrentHashMap 的实例,用来储存注册的 listener
    // 如果传递的参数 listener 为空时,移除 requestKey 对应的 listener
    if (listener == null) {
        mResultListeners.remove(requestKey);
        return;
    }

    // Lifecycle是一个生命周期感知组件,一般用来响应Activity、Fragment等组件的生命周期变化
    final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
    // 当生命周期处于 DESTROYED 时,直接返回
    // 避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
    if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
        return;
    }

    // 开始监听生命周期
    LifecycleEventObserver observer = new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                                   @NonNull Lifecycle.Event event) {
            // 当生命周期处于 ON_START 时开始处理数据
            if (event == Lifecycle.Event.ON_START) {
                // 开始检查受到的数据
                Bundle storedResult = mResults.get(requestKey);
                if (storedResult != null) {
                    // 如果结果不为空,调用回调方法
                    listener.onFragmentResult(requestKey, storedResult);
                    // 清除数据
                    setFragmentResult(requestKey, null);
                }
            }

            // 当生命周期处于 ON_DESTROY 时,移除监听
            if (event == Lifecycle.Event.ON_DESTROY) {
                lifecycle.removeObserver(this);
                mResultListeners.remove(requestKey);
            }
        }
    };
    lifecycle.addObserver(observer);
    mResultListeners.put(requestKey, new FragmentManager.LifecycleAwareResultListener(lifecycle, listener));
}

一、Lifecycle是一个生命周期感知组件,一般用来响应Activity、Fragment等组件的生命周期变化
二、获取 Lifecycle 去监听 Fragment 的生命周期的变化
三、当生命周期处于 ON_START 时开始处理数据,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
四、当生命周期处于 ON_DESTROY 时,移除监听

接下来一起来看一下数据发送的方法,调用了 FragmentManager 的 setFragmentResult 方法

private final ConcurrentHashMap<String, Bundle> mResults = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
        new ConcurrentHashMap<>();

@Override
public final void setFragmentResult(@NonNull String requestKey, @Nullable Bundle result) {
    if (result == null) {
        // mResults 是 ConcurrentHashMap 的实例,用来存储数据传输的 Bundle
        // 如果传递的参数 result 为空,移除 requestKey 对应的 Bundle
        mResults.remove(requestKey);
        return;
    }

    // Check if there is a listener waiting for a result with this key
    // mResultListeners 是 ConcurrentHashMap 的实例,用来储存注册的 listener
    // 获取 requestKey 对应的 listener
    LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
    if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
        // 如果 resultListener 不为空,并且生命周期处于 STARTED 状态时,调用回调
        resultListener.onFragmentResult(requestKey, result);
    } else {
        // 否则保存当前传输的数据
        mResults.put(requestKey, result);
    }
}

一、获取 requestKey 注册的 listener
二、当生命周期处于 STARTED 状态时,开始发送数据
三、否则保存当前传输的数据

源码分析到这里结束了,我们一起来思考一下,在之前我们的都有那些数据传方式。

汇总 Fragment 之间的通信的方式:

  1. 通过共享 ViewModel 或者关联 Activity来完成,Fragment 之间不应该直接通信

  2. 通过接口,可以在 Fragment 定义接口,并在 Activity 实现它

  3. 通过使用 findFragmentById 方法,获取 Fragment 的实例,然后调用 Fragment 的公共方法

  4. 调用 Fragment.setTargetFragment() 和 Fragment.getTargetFragment() 方法,但是注意 target fragment 需要直接访问另一个 fragment 的实例,这是十分危险的,因为你不知道目标 fragment 处于什么状态

  5. Fragment 新的 API, setFragmentResult() 和 setFragmentResultListener()

综合以上通信方式,那么你认为 Fragment 之间通信最好的方式是什么?

这里有我整理出来的一些资料,想要了解更多进阶技术并获取资料可以点击我的github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容