看完你还会觉得你真的会用Fragment吗?

本文的主要目的介绍的是当使用ViewPager时如何查找Fragment的办法,同时介绍一下在使用Fragment时的一些注意事项,以及几种查找方法所适用的场景。

约定

  • 如未特殊说明,本文中的知识点适用于 Activity 重建的时候,即:
public void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState)
    // 略........
    if (savedInstanceState != null) {
    
    
        // 本文讨论的情况
    } else {
    
    
        // 非本文讨论的情况
    }
    // 略........
}
  • 为减少不必要的代码,文章中的 fm、FM 均指代 FragmentManager
  • 如果你已经能熟练的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它们各自的使用场景那么本文可能并不适合你

概述

  • 为什么要复用Fragment 为何避免使用 FM.getFragments
  • FragmentManager.findFragmentById 的使用
  • FragmentManager.findFragmentByTag 的使用 ViewPager 复用之
  • FragmentManager.getFragment 的使用

一、 为什么要复用Fragment
根本原因只有一个:Activity 在重建的时候会恢复其包含的 FragmentManager ,FragmentManager 又会恢复其管理的 Fragment ,同理 Fragment 也会恢复其包含的 FragmentManager,层层递进,直到全部恢复

复用的好处:

避免显示错乱
避免重复添加
避免多余的内存占用
优化界面启动速度

所以复用还是相当有必要的,同时当我们知道了要复用的根本原因之后,如何复用Fragment也就变成 【如何查找已存在的Fragment】的问题了。

二、如何获取已经存在的Fragment
目前我知道的方法如下:

【不推荐】获取全部的已添加到 FragmentManager 的

FragmentManager.getFragments()

根据 TAG 查找 Fragment

FragmentManager.findFragmentByTag(String tag)

根据 Id 查找 Fragment

FragmentManager.findFragmentById(int id)

【重点】根据 Key 查找 Fragment,这个适合与 ViewPager 配合

FragmentManager.getFragment(Bundle bundle,String key)
FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment)

三、谨慎使用FragmentManager.getFragments() 方法
既然不推荐,那总是有原因的,在这个小节会花费比较大的篇幅,我会结合代码告诉你为什么不推荐。

理由一:内容不可控导致Crash
FragmentManager.getFragments() 会返回所有已经添加到 FragmentManager 中的 Fragment,这就可能导致这个列表中包含了非我们自己所定义的Fragment,你可能会有疑问界面上不就显示我自己定义的Fragment么?

首先我们应该清楚的认识到 Fragment 不单单是界面的载体,它也可以用来实现别的功能,比如 生命周期 的监听。比如图片加载库 Glide 以及 Android 最新的 Android 架构组件 中的 ViewModel 都采用了这种方式。

所以如果我们的 Fragment 是和 ViewPager组合使用并且直接将包含这些实例对象(比如 ViewModel 用到 HolderFragment) FragmentManager.getFragments() 的结果丢给 FragmentPagerAdapter 的话那么就会达成本博客的第一项成就:Fragment重复添加

throw new IllegalStateException("Fragment already added: " + fragment)

理由二:顺序不可控
下面的这段代码我相信大家都很熟悉,就算自己没有写过也看别人写过

MainFragment mainFragment = (MainFragment) fm.getFragments().get(0)
// 略.......
SecondaryFragment secondaryFragment = (SecondaryFragment) fm.getFragments().get(1)
// 略.......

这样的写法就会帮助你达成第二项成就:类型转换异常

throw new ClassCastException("Cannot cast android.arch.lifecycle.HolderFragment to MainFragment")

ViewModel相关源码那里可以知道FragmentManager.getFragments() 中包含了其他的Fragment,而这些Fragment的位置往往是不固定,以ViewModel为例,HolderFragment的位置是由初始化的时机决定的。

也就是说你调整了一下 ViewModel 初始化的调用顺序或者在Kotlin项目中将 lateinit 改成了 by lazy 都可能会发生这样的Crash!就 lateinit 改成 by lazy 这条就是我前不久在做项目时真实遇到的。

**理由三:26.x.y 版本中行为发生变更**

在 版本25 中 Activity 是新建的情况下 返回的是 null ,在版本26中返回的是 Collections.EmptyList() ,前面我在维护公司项目时引入了 ROOM 然后有几个界面崩溃了!

此刻我的心情

经过排除发现而问题就出在下面的这段代码中。

mFragments = new ArrayList<>();
if(fm.getFragments() == null){
    
    
    mFragments.add(new MainFragment())
    mFragments.add(new SecondaryFragment())
}else{
    
    
    mFragments.addAll(fm.getFragments())
}
mViewPager.setAdapter(new MyViewPagerAdapter(fm, mFragments))
mTabLayout.setupWithViewPager(mViewPager)
// .....
mTabLayout.getTabAt(0).setText("MainFragment")
// .....

原因就是版本26下,返回的不是 null 导致 mFragments是空的,自然mTabLayout里面是没有Tab的,所以导致了 空针异常,如果这段代码不依赖 getFragments 方法的话其实是没有问题的。

不知道大家有没有注意,如果这个Activity也使用ViewModel,那么还可能会顺带达成上面的 成就一和成就二

扎心了老铁

通过上面的一些例子我们知道了既然直接通过 FM.getFragments() 不可靠,那么通过其他几种方式来获取我们想要找的 Fragment 实例结果如何呢,接着往下看。

四、FM.findFragmentById()
该方法是用过 Fragment 所在的 ViewGroup 的 id(containerViewId) 来查找 Fragment,适合一个 ViewGroup 中只有一个 Fragment 的情况。

方法签名:

public abstract Fragment findFragmentById(@IdRes int id);

用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
    
    
        mainFragment = (MainFragment) getSupportFragmentManager()
                // 这个ID和下面添加 fragment 时指定的 id 要一致
                .findFragmentById(android.R.id.content);
    } else {
    
    
        mainFragment = new MainFragment();
        getSupportFragmentManager().beginTransaction()
                .add(android.R.id.content, mainFragment)
                .commit();
    }
}

:

该方式比较适合 ViewGroup 和 Fragment 是一对一的情况下使用,当不满足该条件时可以使用后面介绍的 findFragmentByTag 方法。
当 一个 ViewGroup 中 有多个 Fragment 时该方法会返回最后添加到该 ViewGroup 的 Fragment。

五、FM.findFragmentByTag()
当一个 ViewGroup 中有多个 Fragment 时 findFragmentById 可能就不是太好使了,这种情况下就需要我们使用 findFragmentByTag 了。

由于是通过 tag 查找已经添加到 FragmentManager 里的 Fragment 实例对象,所以和 containerViewId 也就没有关系了,当然了在我们添加 Fragment 的时候也要注意给 fragment 指定 tag。

方法签名:

public abstract Fragment findFragmentByTag(String tag);
用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
    
    
        mainFragment = (MainFragment) fm.findFragmentByTag(MainFragment.TAG);
    } else {
    
    
        mainFragment = new MainFragment();
        fm.beginTransaction()
                // 在添加的时候给其制定 tag,不然到时候上面的语句就没用了
                .add(android.R.id.content, mainFragment, MainFragment.TAG)
                .commit();
    }
}

上面就是一个很简单的用 TAG 来获取Fragment 的例子,这里需要注意的就是 tag 参数是我们在进行 addreplace 操作的时候指定的。

提示:

  • tag 是可以重复的,因为该参数的之只是 Fragment 的一个成员变量,只是我们无法访问(访问权限 default)。
  • 该方法总是返回 FragmentManager 中和该 tag 一致的最后一个 Fragment。也就是说如果有多个 Fragment 对象使用了同一个 tag 那么最后一个被添加的会被返回,所以不要为不同的 Fragment 对象指定相同的 tag。
  • 不要为同一个 Fragment 实例对象指定在不同的操作中指定不同的 tag,不然会抛出异常,当然这种情况一般是发生在重复添加的情况下

六、与 ViewPager 配合时不要试图使用 FM.findFragmentByTag

上面的 findFragmentByIdfindFragmentByTag 在使用的时候其实都是有一些隐藏限制的:

  • findFragmentById 适用于一个萝卜一个坑的情况
  • findFragmentByTag 使用于 可以指定为 Fragment 指定 tag 情况。

但是很不巧 ViewPager 与这两个情况都匹配不上,原因:

  • 由 ViewPager 所管理的 Fragment 使用的都是同一个 id ,即 ViewPager 的id。
  • 由于 ViewPager 来管理 Fragment 所以我们无法干预其添加移除的过程,所以没有办法为 fragment 指定 tag。

这次针对 ViewPager 的这种情况我要介绍的方法是 FragmentManager.getFragment()方法,与其配套使用的还有一个 FragmentManager.putFragment()方法。

你去搜 【ViewPager find fragment】 可能别人告诉你的 调用 makeFragmentName 生成 tag 或者用 findFragmentByTag(“android:switcher:” + viewPager.getId() + “:” + viewPager.getCurrentItem()) 的那些做法就不要再用了!

// FragmentPagerAdapter.java
private static String makeFragmentName(int viewId, long id) {
    
    
    return "android:switcher:" + viewId + ":" + id;
}

正确的处理姿势示范:

private MainFragment mainFragment;
private SecondaryFragment secondaryFragment;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
    
    
        mainFragment = (MainFragment) fm.getFragment(savedInstanceState, MainFragment.TAG);
        secondaryFragment = (SecondaryFragment) fm.getFragment(savedInstanceState, SecondaryFragment.TAG);
    }
    if (mainFragment == null) {
    
    
        mainFragment = new MainFragment();
    }
    if(secondaryFragment == null){
    
    
        secondaryFragment = new SecondaryFragment()
    }
    // ViewPager 的相关操作
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    
    
    super.onSaveInstanceState(outState);
    if (mainFragment.isAdded()) {
    
    
        fm.putFragment(outState, MainFragment.TAG, mainFragment);
    }
    if (secondaryFragment.isAdded()) {
    
    
        fm.putFragment(outState, SecondaryFragment.TAG, secondaryFragment);
    }
}

两个方法的源码如下:

// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
    
    
    if (fragment.mIndex < 0) {
    
     // 没有被添加到 FragmentManager
        throwException(new IllegalStateException("Fragment " + fragment
                + " is not currently in the FragmentManager"));
    }
    bundle.putInt(key, fragment.mIndex);
}

@Override
public Fragment getFragment(Bundle bundle, String key) {
    
    
    int index = bundle.getInt(key, -1);
    if (index == -1) {
    
    
        return null;
    }
    Fragment f = mActive.get(index);
    if (f == null) {
    
    
        throwException(new IllegalStateException("Fragment no longer exists for key "
                + key + ": index " + index));
    }
    return f;
}

原理解析:

先放两张图,然后结合图片解析

Fragment 在 FragmentManager 中的存储形式

上图只是给出了我们已经知道的,未知的 Fragment 没有表示出来,但不代表不存在

getFragment、putFragment.jpg

以 图中 Fragment A 为例,其他的同理

  1. 当存储状态的时候我们通过putFragment 记录下 FragmentA 的 mIndex, 使用的key 为字符串
    “fragment:A”

  2. 当我们需要查找 A 的时候,先根据 字符串 “fragment:A”(putFragment时使用的值) 去 bundle 中查出我们在 fragmentManager 销毁前记录的 mIndex = 5

  3. 通过 mActivie 中得到 key = 5 的Fragment对象 即:Fragment A

  4. 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 无法访问到所以才需要
    getFragment 和 putFragment。

注意事项:

  • getFragment 和 putFragment 必须成对使用。
  • 在调用 putFragment 方法之前先保证该 fragment 是否已经添加到 FragmentManager 了(即fragment.mIndex >= 0),不然从源码可以得知会抛出异常。

七、总结

  1. 在写 Activity 和 Fragment 的代码时区分区分新建和恢复,在恢复的情况下先查找 Fragment,找不到再创建实例对象
  2. FM.getFragment 适合多个 Fragment 共用一个 ViewGroup 同时还无法为Fragment指定Tag的情况(如ViewPager)
  3. FM.findFragmentById 适合一个 ViewGroup 对应 一个 Fragment 的情况
  4. FM.findFragmentByTag 适合大多数情况,但需要在 add/replace 的时候为每个 Fragment 指定不同 tag
  5. 当有多个 Fragment 对象具有相同的 tag 时,通过 findFragmentByTag 得到的是最后被添加的 Fragment
  6. 当有多个 Fragment 对象共用同意个ViewGroup时,通过 findFragmentById 得到的是最后被添加的 Fragment
  7. putFragment 使用时先判断 Fragment 是否已经添加到 FragmentManager
    最后附上一张图告诉你如何选择合适的方法来查找Fragment

查找Fragment方法选择.jpg

猜你喜欢

转载自blog.csdn.net/A_pyf/article/details/112620538
今日推荐