一道面试题:介绍一下 Fragment 间的通信方式?

news2024/11/23 22:19:35

Fragment 间的通信可以借助以下几种方式实现:

  1. EventBus
  2. Activity(or Parent Fragment)
  3. ViewModel
  4. Result API

1. 基于 EventBus 通信

EventBus 的优缺点都很突出。 优点是限制少可随意使用,缺点是限制太少使用太随意。

因为 EventBus 会导致开发者在架构设计上“不思进取”,随着项目变复杂,结构越来越混乱,代码可读性变差,数据流的变化难以追踪。

所以,规模越大的项目 EvenBus 的负面效果越明显,因此很多大厂都禁止 EventBus 的使用。所以这道题千万不要把 EventBus 作为首选答案,比较得体的回答是:

“ EventBus 具备通信能力,但是缺点很突出,大量使用 EventBus 会造成项目难以维护、问题难以定位,所以我不建议在项目中使用 EventBus 进行通信。 ”

2. 基于 Activity 或父 Fragment 通信

为了迭代更加敏捷,Fragment 从 AOSP 迁移到了 AndroidX ,这导致同时存在着两种包名的 Fragment:android.app.Fragmentandoridx.fragment.app.Fragment

虽然前者已经被废弃,但很多历史代码中尚存, 对于老的Fragment,经常依赖基于 Activity 的通信方式,因为其他通信方式大都依赖 AndroidX 。

class MainActivity : AppCompatActivity() {

    val listFragment: ListFragment by lazy {
        ListFragment()
    }

    val CreatorFragment: CreatorFragment by lazy {
        // 构建Fragment的时候设置 Callback,建立通信
        CreatorFragment().apply { 
            setOnItemCreated { 
                listFragment.addItem(it)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportFragmentManager.beginTransaction().apply {
            add(R.id.fragmentContainer, listFragment)
            commit()
        }
    }
}

如上,在 Activity 或父 Fragment 中创建子Fragment,同时为其设置 Callback

此时,Fragment 的创建依赖手动配置,无法在 ConfigurationChangeed 的时候自动恢复重建,所以除了用来处理 android.app.Fragment 的历史遗留代码之外,不推荐使用。

3. 基于 ViewModel 通信

ViewModel 是目前使用最广泛的通信方式之一,在 Kotlin 中使用时,需要引入fragment-ktx

class ListViewModel : ViewModel() {
    private val originalList: LiveData<List<Item>>() = ...
    val itemList: LiveData<List<Item>> = ...

    fun addItem(item: Item) { 
      //更新 LiveData
    }

}

class ListFragment : Fragment() {
    // 借助ktx,使用activityViewModels()代理方式获取ViewModel
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.itemList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI
        }
    }
}

class CreatorFragment : Fragment() {
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        button.setOnClickListener {
          val item = ...
          viewModel.addItem(item)
        }
    }
}

如上,通过订阅 ViewModel 的 LiveData,接受数据变通的通知。因为两个 Fragment 需要共享ViewModel,所以 ViewModel 必须在 Activity 的 Scope 中创建

关于 ViewModel 的实现原理,相关文章很多,本文不做赘述了。接下来重点看一下 Result API:

4. 基于 Resutl API 通信

Fragment 1.3.0-alpha04起,FragmentManager 新增了 FragmentResultOwner接口,顾名思义 FragmentManager 成为了 FragmentResult 的持有者,可以进行 Fragment 之间的通信。

假设需要在 FragmentA 监听 FragmentB 返回的数据,首先在 FragmentA 设置监听

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // setFragmentResultListener 是 fragment-ktx 提供的扩展函数
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // 监听key为“requestKey”的结果, 并通过bundle获取
        val result = bundle.getString("bundleKey")
        // ...
    }
}

// setFragmentResultListener 是Fragment的扩展函数,内部调用 FragmentManger 的同名方法
public fun Fragment.setFragmentResultListener(
    requestKey: String,
    listener: ((requestKey: String, bundle: Bundle) -> Unit)
) {
    parentFragmentManager.setFragmentResultListener(requestKey, this, listener)
}

当从 FragmentB 返回结果时:

button.setOnClickListener {
    val result = "result"
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

//setFragmentResult 也是 Fragment 的扩展函数,其内部调用 FragmentManger 的同名方法
public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) {
    parentFragmentManager.setFragmentResult(requestKey, result)
}

上面的代码可以用下图表示:

Result API的原理非常简单,FragmentA 通过 Key 向 FragmentManager 注册 ResultListener,FragmentB 返回 result 时, FM 通过 Key 将结果回调给FragmentA 。需要特别注意的是只有当 FragmentB 返回时,result才会被真正回传,如果 setFragmentResult 多次,则只会保留最后一次结果。

生命周期可感知

通过梳理源码可以知道Result API是LifecycleAware的

源码基于 androidx.fragment:fragment:1.3.0

setFragmentResultListener 实现:

//FragmentManager.java
private final Map<String, LifecycleAwareResultListener> mResultListeners =
            Collections.synchronizedMap(new HashMap<String, LifecycleAwareResultListener>());

public final void setFragmentResultListener(@NonNull final String requestKey,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final FragmentResultListener listener) {
        final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
        LifecycleEventObserver observer = new LifecycleEventObserver() {
                if (event == Lifecycle.Event.ON_START) {
                    // once we are started, check for any stored results
                    Bundle storedResult = mResults.get(requestKey);
                    if (storedResult != null) {
                        // if there is a result, fire the callback
                        listener.onFragmentResult(requestKey, storedResult);
                        // and clear the result
                        clearFragmentResult(requestKey);
                    }
                }

                if (event == Lifecycle.Event.ON_DESTROY) {
                    lifecycle.removeObserver(this);
                    mResultListeners.remove(requestKey);
                }
        };
        lifecycle.addObserver(observer);
        LifecycleAwareResultListener storedListener = mResultListeners.put(requestKey,
                new LifecycleAwareResultListener(lifecycle, listener, observer));
        if (storedListener != null) {
            storedListener.removeObserver();
        }
    }
  • listener.onFragmentResultLifecycle.Event.ON_START 的时候才调用,也就是说只有当 FragmentA 返回到前台时,才会收到结果,这与 LiveData 的逻辑的行为一致,都是 LifecycleAware 的

  • 当多次调用 setFragmentResultListener 时, 会创建新的 LifecycleEventObserver 对象, 同时旧的 observer 会随着 storedListener.removeObserver() 从 lifecycle 中移除,不能再被回调。

也就是说,对于同一个 requestKey 来说,只有最后一次设置的 listener 有效,这好像也是理所应当的,毕竟不叫 addFragmentResultListener

setFragmentResult 实现:

 private final Map<String, Bundle> mResults =
            Collections.synchronizedMap(new HashMap<String, Bundle>());

 public final void setFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
        // Check if there is a listener waiting for a result with this key
        LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
        // if there is and it is started, fire the callback
        if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
            resultListener.onFragmentResult(requestKey, result);
        } else {
            // else, save the result for later
            mResults.put(requestKey, result);
        }
    }

setFragmentResult 非常简单, 如果当前是 listener 处于前台,则立即回调 setFragmentResult(), 否则,存入 mResults, 等待 listener 切换到前台时再回调。

一个 listener 为什么有前台/后台的概念呢,这就是之前看到的 LifecycleAwareResultListener 了, 生命周期可感知是因为其内部持有一个 Lifecycle, 而这个 Lifecycle 其实就是设置 listener 的那个 Fragment

 private static class LifecycleAwareResultListener implements FragmentResultListener {
        private final Lifecycle mLifecycle;
        private final FragmentResultListener mListener;
        private final LifecycleEventObserver mObserver;

        LifecycleAwareResultListener(@NonNull Lifecycle lifecycle,
                @NonNull FragmentResultListener listener,
                @NonNull LifecycleEventObserver observer) {
            mLifecycle = lifecycle;
            mListener = listener;
            mObserver = observer;
        }

        public boolean isAtLeast(Lifecycle.State state) {
            return mLifecycle.getCurrentState().isAtLeast(state);
        }

        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
            mListener.onFragmentResult(requestKey, result);
        }

        public void removeObserver() {
            mLifecycle.removeObserver(mObserver);
        }
    }

可恢复重建

mResult 中的数据是会随着 Fragment 的重建可以恢复的,所以 FragmentA 永远不会丢失 FragmentB 返回的结果。当然,一旦 Result 被消费,就会从 mResult 中清除

mResults 的保存

//FragmentManager.java
void restoreSaveState(@Nullable Parcelable state) {
    //...
    ArrayList<String> savedResultKeys = fms.mResultKeys;
        if (savedResultKeys != null) {
            for (int i = 0; i < savedResultKeys.size(); i++) {
                mResults.put(savedResultKeys.get(i), fms.mResults.get(i));
            }
        }
}

mResults 的恢复

Parcelable saveAllState() {
    // FragmentManagerState implements Parcelable
    FragmentManagerState fms = new FragmentManagerState();
    //...
    fms.mResultKeys.addAll(mResults.keySet());
    fms.mResults.addAll(mResults.values());
    //...
    return fms;
}

如何选择?Result API 与 ViewModel

ResultAPI 与 ViewModel + LiveData 有一定相似性,都是生命周期可感知的,都可以在恢复重建时保存数据,那这两种通信方式该如何选择呢?

对此,官方给的建议如下:

The Fragment library provides two options for communication: a shared ViewModel and the Fragment Result API. The recommended option depends on the use case. To share persistent data with any custom APIs, you should use a ViewModel. For a one-time result with data that can be placed in a Bundle, you should use the Fragment Result API.

  • ResultAPI 主要适用于那些一次性的通信场景(FragmentB返回结果后结束自己)。如果使用 ViewModel,需要上提到的 Fragment 共同的父级 Scope,而 Scope 的放大不利于数据的管理。

  • 非一次性的通信场景,由于 FragmentA 和 FragmentB 在通信过程中共存,推荐通过共享 ViewModel 的方式,再借助 LiveData 等进行响应式通信。

5. 跨Activity的通信

最后看一下,跨越不同 Activity 的 Fragmnet 间的通信

跨 Activity 的通信主要有两种方式:

  • startActivityResult
  • Activity Result API

startActivityResult

Result API出现之前,需要通过 startActivityResult 完成通信,这也是 android.app.Fragment 唯一可选的方式。

通信过程如下:

  1. FragmentA 调用 startActivityForResult() 方法之后,跳转到 ActivityB 中,ActivityB 把数据通过 setArguments() 设置给 FragmentB

  2. FragmentB 调用 getActivity().setResult() 设置返回数据,FragmentA 在 onActivityResult() 中拿到数据

此时,有两点需要特别注意:

  1. 不要使用 getActivity().startActivityForResult() , 而是在Fragment中直接调用startActivityForResult()

  2. activity 需要重写 onActivityResult,其必须调用 super.onActivityResult(requestCode, resultCode, data)

以上两点如果违反,则 onActivityResult 只能够传递到 activity 的,无法传递到 Fragment

Result API

1.3.0-alpha02起,Fragment 支持 registerForActivityResult() 的使用,通过 Activity 的 ResultAPI 实现跨 Activity 通信。

FragmentA 设置回调:

class FragmentA : Fragment() {
    private val startActivityLauncher: ActivityResultLauncher<Intent> =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK) {
                //
            } else if (it.resultCode == Activity.RESULT_CANCELED) {
                //
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        startActivityLauncher.launch(Intent(requireContext(), ActivityB::class.java))
    }
}

FragmentB 返回结果

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

了解 Activity Result API 的同学对上述过程应该很熟悉。

简单看一下源码。

源码基于 androidx.fragment:fragment:1.3.0

我们在 FragmentA 中通过创建一个 ActivityResultLauncher,然后调用 launch 启动目标 ActivityB

//Fragment # prepareCallInternal

return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                ActivityResultLauncher<I> delegate = ref.get();
                if (delegate == null) {
                    throw new IllegalStateException("Operation cannot be started before fragment "
                            + "is in created state");
                }
                delegate.launch(input, options);
            }

            //...
        };

可以看到,内部调用了delegate.launch, 我们追溯一下 delegate 的出处,即 ref 中设置的 value

//Fragment # prepareCallInternal

registerOnPreAttachListener(new OnPreAttachedListener() {
            @Override
            void onPreAttached() {
                //ref中注册了一个launcher,来自 registryProvider 提供的 ActivityResultRegistry
                final String key = generateActivityResultKey();
                ActivityResultRegistry registry = registryProvider.apply(null);
                ref.set(registry.register(key, Fragment.this, contract, callback));
            }
        });

    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
        return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
            @Override
            public ActivityResultRegistry apply(Void input) {
                //registryProvider 提供的 ActivityResultRegistry 来自 Activity
                if (mHost instanceof ActivityResultRegistryOwner) {
                    return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
                }
                return requireActivity().getActivityResultRegistry();
            }
        }, callback);
    }

上面可以看到 ref 中设置的 ActivityResultLauncher 来自 Activity 的 ActivityResultRegistry ,也就说 Fragment 的 launch,最终是由其 mHost 的 Activity 代理的。

后续也就是 Activity 的 Result API 的流程了,我们知道 Activity Result API 本质上是基于 startActivityForResult 实现的,具体可以参考这篇文章,本文不再赘述了

总结

本文总结了 Fragment 通信的几种常见方式,着重分析了 Result API 实现原理。 fragment-1.3.0以后,对于一次性通信推荐使用 Result API 替代旧有的 startActivityForResult;响应式通信场景则推荐使用 ViewModel + LiveData (or StateFlow) , 尽量避免使用 EventBus 这类工具进行通信。

下面我针对Android 中不同的技术模块,整理了一些相关的 学习文档笔记 及相应的 面试习题(含答案),大家可以针对性的进行参考学习与复习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/979571.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android T 窗口层级其三 —— 层级结构树添加窗口(更新中)

序 尚未添加窗口的层级结构树&#xff0c;如图 DisplayArea层级结构中的每一个DisplayArea&#xff0c;都包含着一个层级值范围&#xff0c;这个层级值范围表明了这个DisplayArea可以容纳哪些类型的窗口。 每种窗口类型&#xff0c;都可以通过WindowManagerPolicy.getWindowLa…

【LeetCode-中等题】78. 子集

文章目录 题目方法一&#xff1a;动态规划方法二&#xff1a;递归加回溯(关键----startIndex) 题目 注意&#xff1a;这里的nums数组里面的元素是各不相同的&#xff0c;所以不存在去重操作 方法一&#xff1a;动态规划 public List<List<Integer>> subsets(int[]…

linux编译curl库(支持https)

openssl下载和编译 https://www.openssl.org/source/old/ 解压 tar -xvf openssl-3.0.1.tar.gz cd openssl-3.0.1/配置 ./config如果是编译静态库加入 -fPIC no-shared 如果指定安装路径,使用 --prefix=/usr/local/openssl/选项指定特定目录 编译和安装 make sodu make i…

获取图像的属性、图像通道拆分合并实现

获取图像的属性 import numpy as np import cv2 as cv import matplotlib.pyplot as plt#绘制一个全黑的图像 img np.zeros((256,256,3),np.uint8) #创建图像时要指定类型为uint8 plt.imshow(img[:,:,::-1]) plt.show()#等待用户按任意键推出 cv.waitKey(0) cv.destroyAllWi…

基于改进二进制粒子群算法的含需求响应机组组合问题研究(matlab代码)

目录 1 主要内容 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序复现《A Modified Binary PSO to solve the Thermal Unit Commitment Problem》第五章内容&#xff0c;主要做的是一个考虑需求响应的机组组合问题&#xff0c;首先构建了机组组合问题的基本模型&#x…

如何用SSH克隆GitHub项目

诸神缄默不语-个人CSDN博文目录 使用场景&#xff1a;由于不可知的网络问题&#xff0c;无法用HTTPS克隆GitHub项目。 报错fatal: unable to access https://github.com/PolarisRisingWar/llm-throught-ages.git/: GnuTLS recv error (-110): The TLS connection was non-pro…

Nginx__基础入门篇

目录: Nginx的优势 HTTP协议详解 Nginx部署-Yum Nginx配置文件 Nginx编译参数 Nginx基本配置 Nginx日志Log Nginx WEB模块 Nginx 访问限制 Nginx 访问控制 Nginx的优势 Nginx (engine x) 是一个高性能的HTTP(解决C10k的问题)和反向代理服务器&#xff0c;也是一个IM…

如何解决前端传递数据给后端时精度丢失问题

解决精度丢失 有时候我们在进行修改操作时&#xff0c;发现修改既不报错也不生效。我们进行排查后发现服务器端将数据返回给前端时没有出错&#xff0c;但是前端js将数据进行处理时却出错了&#xff0c;因为id是Long类型的&#xff0c;而js在处理后端返回给前端的Long类型数据…

VMware ubuntu空间越用越大

前言 用Ubuntu 1604编译了RK3399的SDK&#xff0c;之后删了一些多余的文件&#xff0c;df - h 已用21G&#xff0c;但window硬盘上还总用了185GB&#xff0c;采用了碎片整理&#xff0c;压缩无法解决 1 启动Ubuntu后, 安装 VMware Tools(T) 、 2 打开ubuntu终端&#xff0c;压…

Jmeter 三种提取方式 —— 关联实例

当请求之间有依赖关系&#xff0c;比如一个请求的入参是另一个请求返回的数据&#xff0c;这时候就需要用到关联处理 Jmeter中常用的关联方法&#xff1a; 正则表达式提取器、XPath提取器、JSON提取器​​​​​​ regex: (.*?)-(.*?)-(.*?)\n.: 匹配除换行符&#xff08;…

Windows下Git Bash的基本使用

创建版本库 git init 初始化完成后&#xff0c;会在目录下创建一个.git的隐藏目录&#xff0c;用来存放项目信息。 、 添加文件到版本库 在项目目录下新建文件readme.txt&#xff0c;内容为 Git is a version control system Git is a free software This is my first Try …

深度学习推荐系统(六)DeepFM模型及其在Criteo数据集上的应用

深度学习推荐系统(六)DeepFM模型及其在Criteo数据集上的应用 在2016年&#xff0c; 随着微软的Deep Crossing&#xff0c; 谷歌的Wide&Deep以及FNN、PNN等一大批优秀的深度学习模型被提出&#xff0c; 推荐系统全面进入了深度学习时代&#xff0c; 时至今日&#xff0c; 依…

uniapp项目实践总结(十一)自定义网络检测组件

导语&#xff1a;很多时候手机设备会突然没网,这时候就需要一个网络检测组件,在没网的时候显示提示用户,提供用户体验。 目录 准备工作原理分析组件实现实战演练案例展示 准备工作 在components新建一个q-online文件夹&#xff0c;并新建一个q-online.vue的组件&#xff1b;…

华为云云服务器评测|安装Java8环境 配置环境变量 spring项目部署 【!】存在问题未解决

目录 引出安装JDK8环境查看是否有默认jar上传Linux版本的jar包解压压缩包配置环境变量 上传jar包以及运行问题上传Jar包运行控制台开放端口访问失败—见问题记录关闭Jar的方式1.进程kill -92.ctrl c退出 问题记录&#xff1a;【!】未解决各种方式查看端口情况联系工程师最后排查…

【C++基础】5. 变量作用域

文章目录 【 1. 局部变量 】【 2. 全局变量 】【 3. 局部变量和全局变量的初始化 】 作用域是程序的一个区域&#xff0c;一般来说有三个地方可以定义变量&#xff1a; 在函数或一个代码块内部声明的变量&#xff0c;称为局部变量。 在函数参数的定义中声明的变量&#xff0c;称…

关于HarmonyOS元服务的主题演讲与合作签约

一、感言 坚持中&#xff0c;总会有很多意想不到的收获。 前几次参与HDC时更多的是观众、开发者、专家的身份&#xff0c;以参观、学习、交流为主。 通过几年的努力&#xff0c;和HarmonyOS功能成长&#xff0c;在2023年的HDC大会中&#xff0c;有了我的演讲&#xff0c;并带领…

永安通配符和泛域名SSL证书的区别

随着互联网的快速发展&#xff0c;现在大多数人都已经习惯在网上交流、购物、学习&#xff0c;因此互联网上的各种类型的网站越来越多&#xff0c;不仅是企事业单位创建各种类型的网站&#xff0c;个人开发者创建的网站也越来越多&#xff0c;一张单域名SSL就不能满足个人或者企…

pdf怎么合并在一起?几种方法快速合并

pdf怎么合并在一起&#xff1f;在处理PDF文件时&#xff0c;有时需要将多个PDF文件合并成一个文件。这种操作在日常学习、工作和生活中很常见。但是&#xff0c;如果没有专业的PDF工具&#xff0c;这项任务可能会变得非常繁琐、耗时和费力。因此&#xff0c;我们需要一款功能强…

java+ssm+mysql电梯管理系统

项目介绍&#xff1a; 使用javassmmysql开发的电梯管理系统&#xff0c;系统包含管理员&#xff0c;监管员、安全员、维保员角色&#xff0c;功能如下&#xff1a; 管理员&#xff1a;系统用户管理&#xff08;监管员、安全员、维保员&#xff09;&#xff1b;系统公告&#…

哈弗猛龙,年轻人的新能源越野车

在 2023 成都车展上&#xff0c;哈弗汽车正式公布猛龙的预售价格&#xff0c;新车的预售价格区间为 16-19 万元&#xff0c;作为一台插电式混合动力车型&#xff0c;新车搭载 1.5THi4 的混动系统&#xff0c;提供纯电续航 102km 和 145km 两个纯电续航版本。 外观造型方面&…