你真的會(huì)用Fragment嗎?Fragment復(fù)用的那些事兒

本文的主要目的介紹的是當(dāng)使用ViewPager時(shí)如何查找Fragment的辦法,同時(shí)介紹一下在使用Fragment時(shí)的一些注意事項(xiàng),以及幾種查找方法所適用的場(chǎng)景。
作者: @怪盜kidou
如需轉(zhuǎn)載不得刪除本文中的任何內(nèi)容(含本段)
如果博客中有不恰當(dāng)之處歡迎在原文中留言交流
http://www.itdecent.cn/p/31f013df7580

大家好,好像距離上次發(fā)布博客好像又過(guò)去了大半年了(額,好像每次發(fā)博客都有這句話),不過(guò)還好我的博客從來(lái)不是以數(shù)量取勝。

我統(tǒng)計(jì)了一下:截止到2018年5月23號(hào),只有11篇文章的博客訪問(wèn)量已經(jīng)超過(guò) 63 萬(wàn)了!感謝大家的支持!

好的屁話不多說(shuō),繼續(xù)看文章

約定

  • 如未特殊說(shuō)明,本文中的知識(shí)點(diǎn)適用于 Activity 重建的時(shí)候,即:
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    // 略........
    if (savedInstanceState != null) {
        // 本文討論的情況
    } else {
        // 非本文討論的情況
    }
    // 略........
}
  • 為減少不必要的代碼,文章中的 fmFM 均指代 FragmentManager
  • 如果你已經(jīng)能熟練的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它們各自的使用場(chǎng)景那么本文可能并不適合你

概述

  • 為什么要復(fù)用Fragment
  • 為何避免使用 FM.getFragments
  • FragmentManager.findFragmentById 的使用
  • FragmentManager.findFragmentByTag 的使用
  • ViewPager 復(fù)用之 FragmentManager.getFragment 的使用

一、 為什么要復(fù)用Fragment

根本原因只有一個(gè):Activity 在重建的時(shí)候會(huì)恢復(fù)其包含的 FragmentManager ,F(xiàn)ragmentManager 又會(huì)恢復(fù)其管理的 Fragment ,同理 Fragment 也會(huì)恢復(fù)其包含的 FragmentManager,層層遞進(jìn),直到全部恢復(fù)

復(fù)用的好處:

  1. 避免顯示錯(cuò)亂
  2. 避免重復(fù)添加
  3. 避免多余的內(nèi)存占用
  4. 優(yōu)化界面啟動(dòng)速度
  5. ........

所以復(fù)用還是相當(dāng)有必要的,同時(shí)當(dāng)我們知道了要復(fù)用的根本原因之后,如何復(fù)用Fragment也就變成 【如何查找已存在的Fragment】的問(wèn)題了。


二、如何獲取已經(jīng)存在的Fragment

目前我知道的方法如下:

  • 【不推薦】獲取全部的已添加到 FragmentManager 的
FragmentManager.getFragments()
  • 根據(jù) TAG 查找 Fragment
FragmentManager.findFragmentByTag(String tag)
  • 根據(jù) Id 查找 Fragment
FragmentManager.findFragmentById(int id)
  • 【重點(diǎn)】根據(jù) Key 查找 Fragment,這個(gè)適合與 ViewPager 配合
FragmentManager.getFragment(Bundle bundle,String key)
FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment)

三、謹(jǐn)慎使用FragmentManager.getFragments() 方法

既然不推薦,那總是有原因的,在這個(gè)小節(jié)會(huì)花費(fèi)比較大的篇幅,我會(huì)結(jié)合代碼告訴你為什么不推薦。

理由一:內(nèi)容不可控導(dǎo)致Crash

FragmentManager.getFragments() 會(huì)返回所有已經(jīng)添加到 FragmentManager 中的 Fragment,這就可能導(dǎo)致這個(gè)列表中包含了非我們自己所定義的Fragment,你可能會(huì)有疑問(wèn)界面上不就顯示我自己定義的Fragment么?

首先我們應(yīng)該清楚的認(rèn)識(shí)到 Fragment 不單單是界面的載體,它也可以用來(lái)實(shí)現(xiàn)別的功能,比如 生命周期 的監(jiān)聽(tīng)。比如圖片加載庫(kù) Glide 以及 Android 最新的 Android 架構(gòu)組件 中的 ViewModel 都采用了這種方式。

所以如果我們的 Fragment 是和 ViewPager組合使用并且直接將包含這些實(shí)例對(duì)象(比如 ViewModel 用到 HolderFragment) FragmentManager.getFragments() 的結(jié)果丟給 FragmentPagerAdapter 的話那么就會(huì)達(dá)成本博客的第一項(xiàng)成就:Fragment重復(fù)添加

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

理由二:順序不可控

下面的這段代碼我相信大家都很熟悉,就算自己沒(méi)有寫(xiě)過(guò)也看別人寫(xiě)過(guò)

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

這樣的寫(xiě)法就會(huì)幫助你達(dá)成第二項(xiàng)成就:類型轉(zhuǎn)換異常

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

ViewModel相關(guān)源碼那里可以知道FragmentManager.getFragments() 中包含了其他的Fragment,而這些Fragment的位置往往是不固定,以ViewModel為例,HolderFragment的位置是由初始化的時(shí)機(jī)決定的。

也就是說(shuō)你調(diào)整了一下 ViewModel 初始化的調(diào)用順序或者在Kotlin項(xiàng)目中將 lateinit 改成了 by lazy 都可能會(huì)發(fā)生這樣的Crash!就 lateinit 改成 by lazy 這條就是我前不久在做項(xiàng)目時(shí)真實(shí)遇到的。

理由三:26.x.y 版本中行為發(fā)生變更

在 版本25 中 Activity 是新建的情況下 返回的是 null ,在版本26中返回的是 Collections.EmptyList() ,前面我在維護(hù)公司項(xiàng)目時(shí)引入了 ROOM 然后有幾個(gè)界面崩潰了!

此刻我的心情

經(jīng)過(guò)排除發(fā)現(xiàn)而問(wèn)題就出在下面的這段代碼中。

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 導(dǎo)致 mFragments 是空的,自然mTabLayout里面是沒(méi)有Tab的,所以導(dǎo)致了 空針異常,如果這段代碼不依賴 getFragments 方法的話其實(shí)是沒(méi)有問(wèn)題的。

不知道大家有沒(méi)有注意,如果這個(gè)Activity也使用ViewModel,那么還可能會(huì)順帶達(dá)成上面的 成就一和成就二

扎心了老鐵

通過(guò)上面的一些例子我們知道了既然直接通過(guò) FM.getFragments() 不可靠,那么通過(guò)其他幾種方式來(lái)獲取我們想要找的 Fragment 實(shí)例結(jié)果如何呢,接著往下看。


四、FM.findFragmentById()

該方法是用過(guò) Fragment 所在的 ViewGroup 的 id(containerViewId) 來(lái)查找 Fragment,適合一個(gè) ViewGroup 中只有一個(gè) 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()
                // 這個(gè)ID和下面添加 fragment 時(shí)指定的 id 要一致
                .findFragmentById(android.R.id.content);
    } else {
        mainFragment = new MainFragment();
        getSupportFragmentManager().beginTransaction()
                .add(android.R.id.content, mainFragment)
                .commit();
    }
}

:

  • 該方式比較適合 ViewGroup 和 Fragment 是一對(duì)一的情況下使用,當(dāng)不滿足該條件時(shí)可以使用后面介紹的 findFragmentByTag 方法。
  • 當(dāng) 一個(gè) ViewGroup 中 有多個(gè) Fragment 時(shí)該方法會(huì)返回最后添加到該 ViewGroup 的 Fragment。

五、FM.findFragmentByTag()

當(dāng)一個(gè) ViewGroup 中有多個(gè) Fragment 時(shí) findFragmentById 可能就不是太好使了,這種情況下就需要我們使用 findFragmentByTag 了。

由于是通過(guò) tag 查找已經(jīng)添加到 FragmentManager 里的 Fragment 實(shí)例對(duì)象,所以和 containerViewId 也就沒(méi)有關(guān)系了,當(dāng)然了在我們添加 Fragment 的時(shí)候也要注意給 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()
                // 在添加的時(shí)候給其制定 tag,不然到時(shí)候上面的語(yǔ)句就沒(méi)用了
                .add(android.R.id.content, mainFragment, MainFragment.TAG)
                .commit();
    }
}

上面就是一個(gè)很簡(jiǎn)單的用 TAG 來(lái)獲取Fragment 的例子,這里需要注意的就是 tag 參數(shù)是我們?cè)谶M(jìn)行 addreplace 操作的時(shí)候指定的。

提示:

  • tag 是可以重復(fù)的,因?yàn)樵搮?shù)的之只是 Fragment 的一個(gè)成員變量,只是我們無(wú)法訪問(wèn)(訪問(wèn)權(quán)限 default)。
  • 該方法總是返回 FragmentManager 中和該 tag 一致的最后一個(gè) Fragment。也就是說(shuō)如果有多個(gè) Fragment 對(duì)象使用了同一個(gè) tag 那么最后一個(gè)被添加的會(huì)被返回,所以不要為不同的 Fragment 對(duì)象指定相同的 tag。
  • 不要為同一個(gè) Fragment 實(shí)例對(duì)象指定在不同的操作中指定不同的 tag,不然會(huì)拋出異常,當(dāng)然這種情況一般是發(fā)生在重復(fù)添加的情況下

六、與 ViewPager 配合時(shí)不要試圖使用 FM.findFragmentByTag

上面的 findFragmentByIdfindFragmentByTag 在使用的時(shí)候其實(shí)都是有一些隱藏限制的:

  • findFragmentById 適用于一個(gè)蘿卜一個(gè)坑的情況
  • findFragmentByTag 使用于 可以指定為 Fragment 指定 tag 情況。

但是很不巧 ViewPager 與這兩個(gè)情況都匹配不上,原因:

  • 由 ViewPager 所管理的 Fragment 使用的都是同一個(gè) id ,即 ViewPager 的id。
  • 由于 ViewPager 來(lái)管理 Fragment 所以我們無(wú)法干預(yù)其添加移除的過(guò)程,所以沒(méi)有辦法為 fragment 指定 tag。

這次針對(duì) ViewPager 的這種情況我要介紹的方法是 FragmentManager.getFragment()方法,與其配套使用的還有一個(gè) FragmentManager.putFragment()方法。

你去搜 【ViewPager find fragment】 可能別人告訴你的 調(diào)用 makeFragmentName 生成 tag 或者用 findFragmentByTag("android:switcher:" + viewPager.getId() + ":" + viewPager.getCurrentItem()) 的那些做法就不要再用了!

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

正確的處理姿勢(shì)示范:

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 的相關(guān)操作
}

@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);
    }
}

兩個(gè)方法的源碼如下:

// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
    if (fragment.mIndex < 0) { // 沒(méi)有被添加到 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;
}

原理解析:

先放兩張圖,然后結(jié)合圖片解析

Fragment 在 FragmentManager 中的存儲(chǔ)形式

上圖只是給出了我們已經(jīng)知道的,未知的 Fragment 沒(méi)有表示出來(lái),但不代表不存在

getFragment、putFragment.jpg

以 圖中 Fragment A 為例,其他的同理

  1. 當(dāng)存儲(chǔ)狀態(tài)的時(shí)候我們通過(guò)putFragment 記錄下 FragmentA 的 mIndex, 使用的key 為字符串 "fragment:A"
  2. 當(dāng)我們需要查找 A 的時(shí)候,先根據(jù) 字符串 "fragment:A"(putFragment時(shí)使用的值) 去 bundle 中查出我們?cè)?fragmentManager 銷毀前記錄的 mIndex = 5
  3. 通過(guò) mActivie 中得到 key = 5 的Fragment對(duì)象 即:Fragment A
  4. 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 無(wú)法訪問(wèn)到所以才需要 getFragment 和 putFragment。

注意事項(xiàng):

  • getFragment 和 putFragment 必須成對(duì)使用。
  • 在調(diào)用 putFragment 方法之前先保證該 fragment 是否已經(jīng)添加到 FragmentManager 了(即fragment.mIndex >= 0),不然從源碼可以得知會(huì)拋出異常。

七、總結(jié)

  1. 在寫(xiě) Activity 和 Fragment 的代碼時(shí)區(qū)分區(qū)分新建和恢復(fù),在恢復(fù)的情況下先查找 Fragment,找不到再創(chuàng)建實(shí)例對(duì)象
  2. FM.getFragment 適合多個(gè) Fragment 共用一個(gè) ViewGroup 同時(shí)還無(wú)法為Fragment指定Tag的情況(如ViewPager)
  3. FM.findFragmentById 適合一個(gè) ViewGroup 對(duì)應(yīng) 一個(gè) Fragment 的情況
  4. FM.findFragmentByTag 適合大多數(shù)情況,但需要在 add/replace 的時(shí)候?yàn)槊總€(gè) Fragment 指定不同 tag
  5. 當(dāng)有多個(gè) Fragment 對(duì)象具有相同的 tag 時(shí),通過(guò) findFragmentByTag 得到的是最后被添加的 Fragment
  6. 當(dāng)有多個(gè) Fragment 對(duì)象共用同意個(gè)ViewGroup時(shí),通過(guò) findFragmentById 得到的是最后被添加的 Fragment
  7. putFragment 使用時(shí)先判斷 Fragment 是否已經(jīng)添加到 FragmentManager

最后附上一張圖告訴你如何選擇合適的方法來(lái)查找Fragment


查找Fragment方法選擇.jpg

我最近剛剛開(kāi)通了微信公眾號(hào)(怪盜kidou),歡迎關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容