深入一点 让细节帮你和Fragment更熟络

0.877字数 9018阅读 2021

有一段时间没有写博客了,作为2017年的第一篇,初衷起始于前段时间一个接触安卓开发还不算太长时间的朋友聊到的一个问题:

“假设,想要对一个Fragment每次在隐藏/显示之间做状态切换时进行监听, 从而在这个时候去完成一些操作,应该怎么去实现呢?"

相信大家听到这类问题第一反应都会觉得是很容易的。而又经过一番讨论过后,发现他的问题场景相对来说比较特殊一点的是:

其想要监听的Fragment是嵌套在另一层Fragment内的子Fragment。这就更有趣了一点,当然了,这个需求场景同样也不会太难实现。

既然这样还写什么博客呢?哈哈。问题本身虽然不算难解决,但个人发现在对其解决的过程中,其实能涉及到不少对于Fragment较实用的小细节。

那么,自己也可以刚好借此机会,重新更加深入的回顾、整理和总结一下关于Fragment的一些使用细节和技巧。何乐而不为呢?特此记录。


<h1>问题场景还原 </h1>

场景还原

如上图所示,这一图例基本上包含了现在大多数主流APP常用的一种UI设计模式。有底部导航,有ViewPager,有侧拉菜单等等。

其实对于这种UI模式,有一个非常直观的印象就是“碎片化”。那么,对应到Android中,用Fragment去实现这种设计就再合适不过了。

那么,我们也就可以看到:在这里,用户的一系列操作就会涉及到大量的Fragment的隐藏和显示的状态切换工作。

从而提归正传,我们试图在这一图例中去模拟的还原一下之前说到的那个问题。首先,我们来分解一下这个用例中的UI设计:

  • 首先自然是主界面,主界面是一个Activity。Ac中有一个底部导航,分为三页,三页自然分别对应了三个Fragment。
  • 第二页和第三页的Fragment界面,我没有去添加额外的设计了,所以十分一目了然,故不加赘述。
  • 重点在第一页,可以看到这一页中有一个侧滑菜单,侧滑菜单里的选项又对应了另外的Fragment界面。
  • 那么,很显然的,侧滑菜单里的界面,就是嵌套在底部导航的第一页Fragment里的另一层Fragment了。
  • 最后,我们可以看到嵌套在侧滑菜单里的第一个子Fragment,它里面是一个ViewPager,于是又涉及到两个新的子Fragment。

OK,到了这里,有了这一番UI分解,我们有了一个大概的了解。现在我们借助一个实际的常用功能更好的还原我们之前说到的那个问题。

假设在底部导航的“第三页”界面中,有一个功能叫做“清除缓存”。那么,使用这一功能就意味着:其它界面当中原先缓存的数据将被清除。

也就意味着,当用户再次切换到另外的界面中时(Fragment由隐藏切换到显示),就需要清除该界面原本的内容,重新获取最新的内容显示。

OK,现在已经回到了我们最初说到的话题了。那么,接着就让我们以这个例子切入,由易到难的看一下:

在常见的各种情况下,应该如何监听Fragment的显示状态切换。而在这其中,又可以注意哪些关于Fragment比较实用的小细节。


<h1>replace与hide/show</h1>

在上一节的图例中,我们说到主界面Activity中有一个底部导航栏,分别对应着三个功能界面(即三个Fragment)。

显然,我们肯定有两种方式来控制这种Fragment的导航切换,即使用replace进行切换,或者通过hide/show来控制切换。

以我们的图例来说,假设我们想要从“第一页”切换到“第二页”,那么我们可以这样做(replace):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_container, new SecondFragment());
ft.commit();

当然也可以这样做(hide/show):

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.hide(new FirstFragment())
  .show(new SecondFragment())
  .commit();

但这里就注意了:如果我们是刚开始接触Fragment,上面的代码看上去似乎没问题,但实际肯定是不能这样去使用hide/show的。

因为就像上述代码表述的一样,我们一定要留意到“new ”,它看上去就像在说:每次隐藏和显示的都是一个全新的Fragment对象。

而事实上也的确如此。所以,这也就意味着:如果这么搞,我们肯定是没办法正确的控制fragment的切换显示的。

那么,我们应该怎么去完成这种需求呢?实际可以提供两种方式,第一种就是为Fragment添加“单例”。

以我们图例中显示的来说,当进入主界面后,优先显示的是“第一页的界面”,所以我们可以先让它显示:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

ft.add(R.id.fragment_container, FirstFragment.getInstance());
ft.add(R.id.fragment_container, SecondFragment.getInstance());
ft.add(R.id.fragment_container, ThirdFragment.getInstance());
ft.hide(SecondFragment.getInstance());
ft.hide(ThirdFragment.getInstance());

ft.commit();

于是在此之后,由“第一页”的Fragment切换到“第二页”的工作,则可以通过类似下面的代码来实现:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
                
ft.hide(FirstFragment.getInstance())
  .show(SecondFragment.getInstance())
  .commit();

你肯定注意到,这里我选择先将会使用到的Fragment对象都添加到内存中去,让暂时不需要显示的碎片先hide。

需要额外说明的是: 这种做法本身其实也是可行的,但在以上的用例里我确实是省方便,而选择了这样的做法。而实际上来说:

个人觉得,如果我们其实并没有让暂时不需要显示的Fragment进行“预加载”的需求的话。那么对应来说:

选择在真正需要切换Fragment显示的时候,再将要显示的Fragment对象进行add,然后控制它们hide和show是更好的做法。

而之所以说这样做更好的原因是什么呢?我们稍微放一放,在之后不久的分析里我们就可以看到。

上述的“单例”这种方式能完成我们的需求吗?当然是可以的。但让人满意吗?似乎总觉得有些别扭。

的确如此,这种方式更像是用Java的方式去解决问题,而非使用Android的方式来解决问题。所以,我们接着看第二种方式,即使用FragmentManager自身来管理我们的Fragment对象。

首先,我们要知道,通过FragmentManager开启事务来动态添加Fragment对象的时候,也是可以为Fragment设置标识的。

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();

FirstFragment first = new FirstFragment();
SecondFragment second = new SecondFragment();
ThirdFragment third = new ThirdFragment();

ft.add(R.id.fragment_container,first, "Tab0");
ft.add(R.id.fragment_container,second,"Tab1");
ft.add(R.id.fragment_container,third,"Tab2");

ft.hide(second);
ft.hide(third);

ft.commit();

上述代码中的“Tab0”这种东西就是我们为Fragment对象设置的标识(Tag),其好处就在于我们之后能够非常方便的控制不同Fragment的切换。

//这里是导航切换的回调
public void onTabSelected(int position) {
    if(position != currentFragmentIndex)
    {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();

        ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
          .show(fm.findFragmentByTag("Tab"+position))
          .commit();
                    
         currentFragmentIndex = position;
     }
 }

就像这里做的,其实FragmentManager本身就有一个List来存放我们add的Fragment对象。这意味着:

我们通过设置的Tag,可以直接复用FragmentManager中已经add过的Fragment对象,而无需所谓的“单例”。

在上述代码中:自定义的currentFragmentIndex用于记录当前所在的导航页索引,position则意味着要切换显示的界面的索引。

那么,再配合上我们对Fragment对象进行add的时候设置的Tag,便能非常方便简洁的实现Fragment的切换显示了。

回到之前说的,我们也可以选择不一次性将三个fragment进行add,而是做的更极致一点。而原理依然非常简单:

            public void onTabSelected(int position) {
                if (position != currentFragmentIndex) {
                    FragmentManager fm = getSupportFragmentManager();
                    FragmentTransaction ft = fm.beginTransaction();

                    Fragment targetFragment = fm.findFragmentByTag("Tab" + position);
                    if (targetFragment == null) {
                        switch (position) {
                            case 1:
                                targetFragment = new SecondFragment();
                                break;
                            case 2:
                                targetFragment = new ThirdFragment();
                                break;
                        }
                         ft.add(R.id.fragment_container,targetFragment,"Tab"+position);
                    }

                    ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
                      .show(targetFragment)
                      .commit();
                    
                    currentFragmentIndex = position;
                }
      }

与之前不同的就是:现在我们最初只add需要显示的FirstFragment。然后在切换Fragment显示的时候,首先通过findFragmentByTag查找对应的Fragment对象是否已经被add进了内存,如果没有则新建该对象,并进行add。而如果已经存在,则可以直接进行复用,并控制隐藏与显示的切换了。

好了,到这里我们看到通过这两种方式都可以实现切换Fragment显示的需求。那么,其差别在哪里呢?我们可以简单的到源码中找下答案。


<h2>从源码看replace与add的异同</h2>

首先,我们可以明确一点的是:无论是使用到replace还是使用hide/show,前提都需要确保将相关的Fragment对象放入FragmentManager。

那么,在之前的用例描述中:对于hide/show的使用来说,我们知道首先是通过add的方式来进行的。就像如下代码所做的这样:

ft.add(R.id.fragment_container, first, "Tab0");

那么,对于replace来说又是如何呢?其实我们可以打开replace方法的源码看一看说明:

以上截图是源码中对于replace方法的注释说明,我们阅读一下发现:简单的来说,它似乎是在告诉我们,调用replace方法,效果基本就等同于:

先调用remove方法删除掉指定containerViewId中当前所有已添加(add)的fragment对象,然后再通过add方法将replace传入的对象添加。

看到这里,我们难免在想,这么说其实replace和add在本质上来说,是很相似的,其实这样说也没错。通过以下代码可以验证上面的结论。

Log.d(TAG,getSupportFragmentManager().getFragments().size()+"");

通过以上代码可以获取当前FragmentManager中存放的有效的fragment对象的数量,那么对于我们上面说到的用例中:

  • 当使用replace控制fragment的显示时,会发现获取到的碎片数始终是1。因为每次replace时,都会将之前存在的fragment对象remove掉。
  • 当使用hide/show控制时,获取到的碎片数将与我们进行add的数量相同。比如之前我们在首页add了3个fragment,获取到的数量就是3。

那么,fragment内部究竟是怎样的机制,才会造成这样的结果呢?回顾一下:
其实我们在管理fragment的时候,始终在和两个东西打交道,那就是:FragmentManager与FragmentTransaction。

FragmentManager其实是一个抽象类,它的具体实现是FragmentManagerImpl,其内部有这样的东西:

ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
//......

我们前面说到FragmentManager自身就有一个集合来存放fragment对象,其实就是上面这样的东西。

FragmentTransaction其实也是一个抽象类,通过FragmentManagerImpl其中的代码,我们可以知道其具体实现:

是的,FragmentTransaction的具体实现其实是一个叫做BackStackRecord的类。由此为基础,我们就可以看看add和replace究竟做了什么样的工作。


可以看到add和replace很重要的一个的区别在于某种行为标识:“OP_ADD”与“OP_REPLACE”。

但它们二者最终都是来到一个叫做doAddOp的方法,截取这个方法内目的性最强的部分代码如下:

可以看到这里做的其实就是创建一个Op类型的对象,然后执行addOp方法。那么,我们首先看看Op是个什么东西?

好吧,连我数据结构这么渣的,也看出这就是一个链表结构的东东啦。其实不难理解,因为我们在正式commit事务之前:

其实可以执行一系列的add,replace,hide,remove等等的操作,所以肯定是需要一个类似链表这样的数据结构来更好的记录这些信息的。

那么,至于addOp这个方法就不难想象了,其实就是通过修改链表节点信息来记录所做的类似add这样的操作。节省篇幅,就不贴源码截图了。

但问题是,到现在我们还没有和之前说到的FragmentManager产生联系。这是没错的,因为真正和FM产生联系,自然是在commit之后。

这里由于能力和篇幅有限,就不会做详细的逐步分析了,总之我们明白一点:BackStackRecord本身实现了Runnable接口。接着,在commit之后的一系列相关调用之后,最终则会进入到BackStackRecord的run()方法开始执行。

那么,现在我们截取run()方法内我们关心的部分代码来看看:

              switch (op.cmd) {
                case OP_ADD: {
                    Fragment f = op.fragment;
                    f.mNextAnim = enterAnim;
                    mManager.addFragment(f, false);
                } break;
                case OP_REPLACE: {
                    Fragment f = op.fragment;
                    int containerId = f.mContainerId;
                    if (mManager.mAdded != null) {
                        for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
                            Fragment old = mManager.mAdded.get(i);
                            if (FragmentManagerImpl.DEBUG) Log.v(TAG,
                                    "OP_REPLACE: adding=" + f + " old=" + old);
                            if (old.mContainerId == containerId) {
                                if (old == f) {
                                    op.fragment = f = null;
                                } else {
                                    if (op.removed == null) {
                                        op.removed = new ArrayList<Fragment>();
                                    }
                                    op.removed.add(old);
                                    old.mNextAnim = exitAnim;
                                    if (mAddToBackStack) {
                                        old.mBackStackNesting += 1;
                                        if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
                                                + old + " to " + old.mBackStackNesting);
                                    }
                                    mManager.removeFragment(old, transition, transitionStyle);
                                }
                            }
                        }
                    }
                    if (f != null) {
                        f.mNextAnim = enterAnim;
                        mManager.addFragment(f, false);
                    }
                } break;

首先是OP_ADD,可以看到处理的代码非常简单,关键的就是那句:

mManager.addFragment(f, false);

我们回到FragmentManagerImpl来中看看这句代码究竟做了什么:

    public void addFragment(Fragment fragment, boolean moveToStateNow) {
        if (mAdded == null) {
            mAdded = new ArrayList<Fragment>();
        }
        if (DEBUG) Log.v(TAG, "add: " + fragment);
        makeActive(fragment);
        if (!fragment.mDetached) {
            if (mAdded.contains(fragment)) {
                throw new IllegalStateException("Fragment already added: " + fragment);
            }
            mAdded.add(fragment);
            fragment.mAdded = true;
            fragment.mRemoving = false;
            if (fragment.mHasMenu && fragment.mMenuVisible) {
                mNeedMenuInvalidate = true;
            }
            if (moveToStateNow) {
                moveToState(fragment);
            }
        }
    }

这个方法其实也不复杂,关键的信息就是将fragment对象加入mAdded集合中,而makeActive则会将其加入到mActive集合。
(之前说到的通过getFragmentManager.getFragments这句代码返回的其实就是mActive这个集合)

而最后的moveToState就是真正关键的改变fragment对象状态的操作了,这个方法比较复杂,这里不深入分析了。

现在我们回头继续看run方法中OPP_REPLACE执行的操作如何,事实上有了之前的基础,我们不难发现:
replace的不同之处就在于,会先把mManager中mAdded集合内相同contanierViewID的fragment对象遍历出来删除掉:

mManager.removeFragment(old, transition, transitionStyle);

然后最后就像我们之前知道的那样,其实依旧是把fragment对象添加到mManager的集合中去:

if (f != null) {
   f.mNextAnim = enterAnim;
   mManager.addFragment(f, false);
  }

现在我们应该明白,replace和add为什么会造成在mManger中存放的数量不同以及源码中replace方法的注释说明的原因了。但继续延伸吧。

回忆一下:我们知道FragmentTransaction实际上还有一个功能叫做addToBackStack(),故名思议,就是说将Fragment对象加入返回栈。

其实这个方法的实际操作,在之前我们分析过的源码中也可以得知。在BackStackRecord的run方法中,对于OPP_REPLACE操作有如下代码:

if(mAddToBackStack)就是指使用了addToBackStack方法的情况,这时执行的一个操作是将fragment对象的mBackStackNesting变量自增。
随后紧接着的操作就是通过mManager去removeFragment,重点就在这里,让我们看看removeFragment方法的源码:

注意final boolean inactive = !fragment.isInBackStack();这行代码,其具体实现为:

也就是说,由于addToBackStack的存在,会导致inactive的获取结果为false。所以这时根本不会真正去执行if语句块里真正删除fragment对象的操作。

这显然是符合逻辑的,将fragment对象加入返回栈,意味着我之后还可能会使用到该对象,你自然不能像之前一样把它们清除掉。

当然,我们还可以自己在addToBackStack使用之后,再通过fragmentManager.getFragments.size()去验证一下获取到的fragment对象的数量变化。


<h2>生命周期的不同</h2>

我们现在已经知道当通过add,replace,remove等操作时,最终会通过moveToState去改变对应fragment对象的状态。

那么,hide/show又是如何呢?于是我们又回到了BackStackRecord的run方法当中寻觅答案:

那我们就以hideFragment作为例子,看看这时mManager究竟是做的什么工作呢?

可以看到这时实际上就告别了moveToState,其本质在于通过改变fragment对象的mView的可视性来控制显示,并回调onHiddenChanged。

以上的分析的目的为何呢?其实就是为了证明,通过replace和hide/show两种方式,最大的区别就在于:二者的生命周期变化相去甚远。

我们还是可以自己去验证这点。最简单的方式是:写一个基类的BaseFragment,然后为所有生命周期回调添加日志打印,就类似于如下这样:

public class BaseFragment extends Fragment{

    protected String clazzName = this.getClass().getSimpleName() +" ==> ";

    @Override
    public void onAttach(Context context) {
        Log.d(clazzName,"onAttach");
        super.onAttach(context);
    }
    
    //......
}

那么,当我们使用replace进行切换显示的时候,会发现其生命周期的路线类似于下面这样:

然后我们切换到“hide/show”来观察生命周期的变化,发现其回调如下所示:

而使用replace时,是否使用addToBackStack的另一个区别也是生命周期上的不同。

没有使用addToBackStack的时候,被切换的对象和切换进来的对象的生命周期分别为:

  • onPause → onStop → onDestroyView → onDestroy → onDeatch
  • onAttach → onCreate → onCreateView → onViewCreated → onActivityCreated → onStart → onResume

而当使用了addToBackStack后,被切换的对象的生命周期变化则成了:

  • onPause → onStop → onDestroyView

而切换进行的对象,如果是首次进行切换,则与之前无异。反之,如果已经存在于返回栈内,生命周期变化则成了:

  • onCreateView → onViewCreated → onActivityCreated → onStart → onResume

现在我们回到之前说到的一个问题,为什么说不需要在最初就把潜在的几个Fragment一股脑进行add,有了之前分析的基础,我们知道:

当我们把fragment进行add过后,最终会执行到moveToState方法进行状态设置,那对应到我们之前的例子中来说的话:

就代表着我们最初添加的三个fragment对象,都会经历onAttach → onCreate → ...... → onResume这一初始化生命周期。

这意味着添加的三个fragment对象中,“第二页”与“第三页”虽然目前不用显示,但系统需要耗费时间去完成它们的初始化周期。

这显然在一定程度上会影响效率。当然,具体要怎么使用其实还是看实际的需求哪种更合适。我们只要明白其中的细节就可以了。

现在,我们思考一个问题,对于我们本文中的图例应用来说,究竟使用replace还是hide/show更适合呢?其实有了之前的基础,我们知道:

ft.add(R.id.fragment_container,first, "Tab0");

这行代码的效果,其实完全可以用这样使用replace来转换:

 ft.replace(R.id.fragment_container,first,"Tab0").addToBackStack(null);

但是,我们前面也说到了,最大的区别就在于二者生命周期变化的不同。再分析一下:

  • 首先,如果我们使用的是replace切换fragment的显示,那显然我们需要使用addToBackStack。否则就无需谈什么监听由隐藏到显示了,因为单独replace每次都意味着切换进的是一个全新的fragment对象。
  • 如果使用replace+addToBackStack,那么被切换的fragment对象(即隐藏的对象)与切换进的fragment对象(即显示的对象)的生命周期变化路线我们都已经清楚了,这时就有点类似监听Activity了。
  • 使用hide/show来控制显示切换,显然是最简单的,监听onHiddenChange回调,实现自己的目的就可以了。

上述的第2、3种方式都能实现目的,但replace最大的缺陷就在于每次切换都会进入onCreateView到onResume这一周期。

这其中总会涉及到我们在fragment这些生命周期中做出的一些例如数据初始化的操作等,显然这样控制起来是非常麻烦的(数据重复加载等)。

所以,综合比较之下,显然hide/show才是最合适的方式。而对于replace来说,最适合的显然就还是那种比较典型的例子:
比如pad上的新闻应用,左边是新闻列表,右边为新闻的详细内容,这时右边的fragment用replace来切换新闻内容显示就是最合适的。

那么回到我们本文之前图例里的演示应用,那么比如在“第三页”清除缓存后,切换到了“第二页”。
这时使用hide/show切换fragment显示,然后通过onHiddenChange完成监听就非常简单了。

这就是这个图例里,针对于我们最初提出的问题可以演示的第一种情况,也是最简单的一种情况。
当然了,这也是因为图例中,首页底部导航对应的三个fragment都隶属于同一级,即主Activity当中。


<h1>getFragmentManager还是getChildFragmentManager()?</h1>

现在阶段性总结一下,本文图例中,首页导航的三个fragment都属于同一个FragmentMnanger管理。所以根据之前的源码分析我们就可以得知:

在我们通过hide/show来切换两个碎片显示时,相对应的,它们的onHiddenChaned方法就会被回调,所以这个时候监听它们的隐藏/显示是很容易的。

那我们更进一步,比较特殊的是图例中“第一页”的界面,由图可以看到其中有一个侧滑菜单,菜单中的三个选项卡对又应另外三个fragment。

OK,那么现在思考一下!这三个fragment还和我们之前说到的首页导航对应的三个fragment位于同一级别吗?我们来分析一下。

首先,我们已经说到在代码中动态的控制fragment,都借助于FragmentManager。而对应“第一页”的FirstFragment本身也是一个fragment。

所以在这个时候,与之前我们在主Activity通过get(Support)FragmentManager有一点不同的是,在Fragment当中我们多了一个选择:

我们发现有趣的是多了一个叫做getChildFragmentManager的方法,它们之间到底区别在哪呢?在主Activity和FirstFragment分别添加下如下日志:

// Activity
Log.d(TAG, getSupportFragmentManager()/*或者getFragmentManager()*/+"");
// FirstFragment
Log.d(TAG,getFragmentManager()+"\n"+getChildFragmentManager());

然后运行程序发现如下的日志打印:

由此我们可以发现,在FirstFragment中获取的FragmentManager和之前在MainActivity中的是同一对象,归属于一个叫做HostCallBacks的东西管理。

与之不同的是:通过getChildFragmentManager获取到的FragmentManager则是另一个不同的对象,而另一个不同在于它则属于FirstFragment自身。

(P.S:关于HostCallBack这个东西,有兴趣的朋友可以自己研究源码或者关于Fragment源码分析的文章。简单的来说,它是属于FragmentActivity的内部类,getSupportFragmentManager实际就是通过控制HostCallback返回FragmentManagerImpl对象)

好的,那么现在由此其实不难想象,既然Fragment会多出这么一个特定的方法,肯定是有其存在的意义的。

现在假定我们依然使用FragmentMananger在FirstFragment中管理侧滑菜单的子碎片,那么首先可能会出现如下所示的问题:

这里可以看到的一个问题就是:当我们在底部导航由第一页转至第三页后,第一页的fragment中间的内容仍然没有消失。

其实原因不难分析,因为前面说到如果此时使用getFragmentManager,意味着此时获取到的FM对象其实和MainActivity中使用的FM是同一个对象。

也就是说,如此进行添加,侧滑菜单对应的三个Fragment其实仍然是被add进了与FirstFragment隶属的相同的FragmentManager的集合内。

那么,假定我们把侧滑菜单对应的第一个fragment对象命名为Child1Fragment,这其实也就意味着:

我们视觉上看上去属于第一页(即FirstFragment)的内容其实本来真正应该是属于Child1Fragment的内容。
但由于我们通过getFragmentManager进行add操作,我们通过如下代码完成前面说到由第一页跳转至第三页的操作则会导致:

ft.hide(first)
  .show(third)
  .commit;

虽然我们按照逻辑hide了FirstFragment对象,但关键在于:因为它们都属于同一个FM对象,所以其实Child1Fragment仍然没有被hide。

由此其实我们不难想象:如果通过FragmentManager在Fragment中嵌套Fragment,将由于逻辑的严重混乱,而造成难以管理

那么,与之对应的,如果选择使用getChildFragmentManager的好处有哪些呢?我们可以简单的概括一下:

首先,最重要的一点:通过ChildFragmentManager进行管理的子Fragment对象,与其父Fragment对象的生命周期是息息相关的

举个例子,假设我们用SecondFragment对象来replace掉FirstFragment对象,这时候有了前面的基础,我们都知道:

FristFragment将走入onPause开始的这段生命周期。而使用ChildFragmentManager的好处在于,其内部的子Fragment也会受相同的生命周期管理。

显然,我们可以预见由此带来的最大的好处就是:此时各个Fragment之间的逻辑清晰,层级分布明确,将大大利于我们对其进行管理

另一个非常实用的好处就在于:在这种管理模式下,子Fragment能够很容易的实现与父Fragment之间进行通信。通过一个例子能更形象的理解。

依旧是本文最初的图例,我在FirstFragment中放置了一个ToolBar,那么如果我切换了选项卡,想要在子Fragment的动态的操作Toolbar,就能这么做:

    public void doSomething(){
        // do what you want to do
    }

是的,首先在FirstFragment中我们提供这一样一个回调方法,然后在子Fragment中我们就可以通过如下方式与其发生互动:

        FirstFragment parent = (FirstFragment) getParentFragment();
        parent.doSomething();

这都是一些非常实用的小技巧,更多的延伸实用,我们可以自己在实际中拓展。总之:如果是刚开始接触Fragment,一定记住:

如果是在Fragment中添加Fragment时,请一定记住选择通过getChildFragmentManager来对碎片进行管理

那么,现在我们言归正传。通过ChildFragmentManager是不是就能解决我们说到的监听fragment隐藏/显示了呢?

其实不难推测出答案。我们再次回到那个情景,在第三页清楚缓存后,回到第一页,那么很明显符合我们逻辑的实现就是:

ft.hide(third)
  .show(first)
  .commit;

现在我相信我们都很清楚了,这时候通过onHiddenChanged肯定是能够监听到FirstFragment的,但对于嵌套在其内的Child1Fragment就不行了。

但是因为之前的基础,这个问题显然已不难解决。我们可以在FirstFragment中定义一个index来记录此时的侧滑菜单的子Fragment索引,随后:

    // FirstFragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
        getChildFragmentManager().getFragments().get(currentIndex).onHiddenChanged(hidden);
    }

怎么样,是不是很简单呢?这就是我们本文中提到的问题的第二种场景延伸。所以说细节真的很能够帮助我们更加灵活的掌握一件事物。


<h1>与ViewPager的配合</h1>

现在我们进一步深入,正如本文图例所示,Child1Fragment中有一个ViewPager,ViewPager中有放置了两个Fragment。

再次衍生我们之前的场景描述:现在在“第三页”清除缓存过后,再次回到第一页,此时第一页显示的正好是ViewPager中的碎片。

那么,这个时候我们应该如何监听对应的这个位于ViewPager中的Fragment对象呢?相信有了之前的基础,我们很容易类推出来。

既然上一节中我们已经将显示状态的改变由FirstFragment传递到了Child1Fragment,那么现在只需要继续向ViewPager进行传递就行了。

    // Child1Fragment.java
    @Override
    public void onHiddenChanged(boolean hidden) {
       getChildFragmentManager().getFragments().get(mViewPager.getCurrentItem()).onHiddenChanged(hidden);
    }

这便是我们本文描述的问题的第三种场景延伸。但是关于fragment配合viewpager使用时,依然还有很多值得留意的小技巧和细节。

<h2>ViewPager的缓存机制</h2>

通过之前的分析与总结,我们已经清楚通过FragmentManager管理碎片时,Fragment的生命周期变化情况。

那么,当Fragment配合ViewPager时,Fragment的生命周期又是什么情况呢?我们还是可以自己验证一下。

为了能得到更准确的结论,可以把Child1Fragment中ViewPager放置的Fragment数量添加到4个,再通过切换来查看各个碎片的生命周期。

为了节省篇幅,我们选择查看两个最具有代表性的生命周期变化的片段截图,首先是ViewPager的初始显示时的片段截图:

可以看到虽然ViewPager初始时,只需要显示第一个Fragment,但是第二个Fragment对象仍然经过了初始化的生命周期。接着:

假设我们进一步的操作是直接将ViewPager由第一个Fragment滑动至第三个,然后我们再来瞧一瞧对应的生命周期变化:

由此我们可以发现,这个时候不仅切换到的第三个Fragment进行了初始化,与它相邻的第四个碎片同样也进行了初始化。与此同时,可以发现:
在这之前显示的第一个Fragment则经过了onPause到onDestoryView的生命周期变化,也就是说这时第一个Fragment的视图会被销毁。

这其实就是ViewPager自身的一个缓存机制,默认情况下它会帮我们缓存一个Fragment相邻的两个Fragment对象。简单来说,就像上面表现的:

当第一个Fragment需要显示时,其相邻的第二个对象也会进行初始化。第三个Fragment需要显示时,左边第二个对象已经完成了初始化,于是右边的第四个则会进行初始化。

我们不难推测出设计者如此设计的初衷:显然这是为了用户对ViewPager有更好的体验,设想一下:

  • 当用户进入到ViewPager的第一个视图,这时相邻的第二个视图也已经进行了初始化。那么当用户切换到第二页,则可以直接进行浏览了。
  • 当用户切换到第三页的时候,之所以选择销毁掉第一页的视图,则是为了减少嵌入的Fragment数量,减少滑动时出现卡顿的可能性。

<h2>setOffscreenPageLimit</h2>

了解了ViewPager的缓存机制,则有一个比较实用的东西叫做setOffscreenPageLimit,它的作用就是来设置这个缓存的上限。

这个上限的默认值为1,而当该值为1时,其效果就和我们上一节描述的一样。我们可以自己设置该值来改变这个缓存的数量。

但与此同时,需要注意的另一个细节是,要避免做出类似如下代码所示的这种想当然的操作:

mViewPager.setOffscreenPageLimit(0);

这行代码是无法完成你本来想要实现的目的的,究其原因,可以在源码中找到答案:

从代码中不难看到,当我们传入的limit参数小于DEFAULT_OFFSCREEN_PAGES时,就将直接被设置为等同于这个默认值。

那么DEFAULT_OFFSCREEN_PAGES的值究竟是多少呢?其实从前面的分析就能得出结论,其值为1:

<h2>setUserVisibleHint</h2>

我们也许已经留意到,在前面的生命周期变化中,有一个叫做setUserVisibleHint的东西反复出现了不少次。其实有了之前的基础,就容易理解了。

之前通过FragmentManager控制碎片的隐藏和显示,回调的是onHiddenChanged方法。而在ViewPager则没有通过FM来进行控制。
所以不难推测,这时Fragment对象的显示和隐藏,回调多半就不是onHiddenChanged了。事实正是如此,此时的回调则是setUserVisibleHint。

所以说,如果想要在这种情况监听Fragment对象的隐藏/显示,那么监听这个方法就可以了。这也理解为我们本文提出的问题的第四种场景延伸。

最后,这里注意一下这种情景与我们之前描述的第三种场景的区别。之前说到的对于ViewPager中的碎片的显示/隐藏状态监听的解决方案,是从针对从其它Fragment切换到ViewPager中的某个Fragment显示的情景。而监听setUserVisibleHint则是针对于都是位于ViewPager内的Fragment对象相互之前的切换显示的情况。


<h2>ViewPager的懒加载</h2>
其实写到这里,对于我能想到的关于本文最初说到的那个朋友提出的问题 常见的情景延伸都已经总结到了,本想结束。

但是前面说到ViewPager的缓存机制时,我们提到ViewPager会根据设置的缓存数量上限来控制相应数量的Fragment对象提前初始化。

这就可能涉及到另一个比较实用的小技巧:配合ViewPager时,Fragment的懒加载。虽然网上该类资料很多,但还是可以简单总结一下。

所谓的懒加载其实很好理解,我们说了正常情况下,ViewPager会对某指定数量的Fragment进行预初始化。

而通常在Fragment初始化的生命周期里:我们都会做一些与该Fragment相关的数据的加载工作等等。那么:
在一些时候,比如某个Fragment在初始化时需要加载的数据量较大;或者说因为数据来源于网络等原因,
此时等待该Fragment完成初始化,就会从一定程度上影响到应用的效率,这个时候就产生了所谓的“懒加载”的需求。

所以其实懒加载的本质非常简单:那就是不要在ViewPager预初始化的时候去加载数据,而是当该Fragment真正显示时才进行加载。

而因为我们之前已经知道了setUserVisibleHint这个东西,所以其实解决方案就不难给出了。这里可以给出一个简单的模板,仅供参考:

public abstract class LazyLoadFragment extends Fragment{

    protected boolean isPrepared;
    protected boolean isLoadedOnce;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutId(), container, false);

        isPrepared = true;
        // 数据加载
        loadData();

        return view;
    }

    protected abstract int getLayoutId() ;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        loadData();
    }

    protected void loadData() {
        if(!isPrepared || !getUserVisibleHint() || isLoadedOnce)
            return;
        // 懒加载
        lazyLoad();
        isLoadedOnce = true;
    }

    protected abstract void lazyLoad();

    @Override
    public void onDetach() {
        super.onDetach();
        isPrepared = isLoadedOnce = false;
    }

}

上面的代码应该不难理解,如果需要实现懒加载,则可以让Fragment继承该类,然后再覆写用于加载数据的lazyLoad方法就可以了。

推荐阅读更多精彩内容