简要的说一下:Fragment 间的通信方式?

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 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有想参考小编 PDF学习文档的如果有需要的可以 私信回复我 666 即可货取!!! 里面记录许多Android 相关学习知识点。

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具