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

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

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

我統(tǒng)計了一下:截止到2018年5月23號疼鸟,只有11篇文章的博客訪問量已經(jīng)超過 63 萬了!感謝大家的支持庙曙!

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

約定

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

概述

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

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

根本原因只有一個:Activity 在重建的時候會恢復(fù)其包含的 FragmentManager 镣隶,F(xiàn)ragmentManager 又會恢復(fù)其管理的 Fragment ,同理 Fragment 也會恢復(fù)其包含的 FragmentManager什荣,層層遞進矾缓,直到全部恢復(fù)

復(fù)用的好處:

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

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


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

目前我知道的方法如下:

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

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

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

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

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

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

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

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

理由二:順序不可控

下面的這段代碼我相信大家都很熟悉邮旷,就算自己沒有寫過也看別人寫過

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

這樣的寫法就會幫助你達成第二項成就:類型轉(zhuǎn)換異常

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

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

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

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

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

此刻我的心情

經(jīng)過排除發(fā)現(xià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里面是沒有Tab的辱揭,所以導(dǎo)致了 空針異常离唐,如果這段代碼不依賴 getFragments 方法的話其實是沒有問題的。

不知道大家有沒有注意问窃,如果這個Activity也使用ViewModel亥鬓,那么還可能會順帶達成上面的 成就一和成就二

扎心了老鐵

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


四、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 是一對一的情況下使用,當(dāng)不滿足該條件時可以使用后面介紹的 findFragmentByTag 方法尉姨。
  • 當(dāng) 一個 ViewGroup 中 有多個 Fragment 時該方法會返回最后添加到該 ViewGroup 的 Fragment庵朝。

五、FM.findFragmentByTag()

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

由于是通過 tag 查找已經(jīng)添加到 FragmentManager 里的 Fragment 實例對象,所以和 containerViewId 也就沒有關(guān)系了覆致,當(dāng)然了在我們添加 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 參數(shù)是我們在進行 addreplace 操作的時候指定的儡羔。

提示:

  • tag 是可以重復(fù)的,因為該參數(shù)的之只是 Fragment 的一個成員變量璧诵,只是我們無法訪問(訪問權(quán)限 default)汰蜘。
  • 該方法總是返回 FragmentManager 中和該 tag 一致的最后一個 Fragment。也就是說如果有多個 Fragment 對象使用了同一個 tag 那么最后一個被添加的會被返回腮猖,所以不要為不同的 Fragment 對象指定相同的 tag鉴扫。
  • 不要為同一個 Fragment 實例對象指定在不同的操作中指定不同的 tag赞枕,不然會拋出異常澈缺,當(dāng)然這種情況一般是發(fā)生在重復(fù)添加的情況下

六坪创、與 ViewPager 配合時不要試圖使用 FM.findFragmentByTag

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

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

但是很不巧 ViewPager 與這兩個情況都匹配不上姐赡,原因:

  • 由 ViewPager 所管理的 Fragment 使用的都是同一個 id 莱预,即 ViewPager 的id。
  • 由于 ViewPager 來管理 Fragment 所以我們無法干預(yù)其添加移除的過程项滑,所以沒有辦法為 fragment 指定 tag依沮。

這次針對 ViewPager 的這種情況我要介紹的方法是 FragmentManager.getFragment()方法,與其配套使用的還有一個 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;
}

正確的處理姿勢示范:

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

兩個方法的源碼如下:

// 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;
}

原理解析:

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

Fragment 在 FragmentManager 中的存儲形式

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

getFragment辜限、putFragment.jpg

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

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

注意事項:

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

七、總結(jié)

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

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


查找Fragment方法選擇.jpg

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泉懦,一起剝皮案震驚了整個濱河市稿黍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌崩哩,老刑警劉巖巡球,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異邓嘹,居然都是意外死亡酣栈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門汹押,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矿筝,“玉大人,你說我怎么就攤上這事棚贾〗盐” “怎么了榆综?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長铸史。 經(jīng)常有香客問我鼻疮,道長,這世上最難降的妖魔是什么琳轿? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任判沟,我火速辦了婚禮,結(jié)果婚禮上崭篡,老公的妹妹穿的比我還像新娘挪哄。我一直安慰自己,他們只是感情好琉闪,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布中燥。 她就那樣靜靜地躺著,像睡著了一般塘偎。 火紅的嫁衣襯著肌膚如雪疗涉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天吟秩,我揣著相機與錄音咱扣,去河邊找鬼。 笑死涵防,一個胖子當(dāng)著我的面吹牛闹伪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播壮池,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼偏瓤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了椰憋?” 一聲冷哼從身側(cè)響起厅克,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橙依,沒想到半個月后证舟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡窗骑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年女责,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片创译。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡抵知,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情刷喜,我是刑警寧澤残制,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站吱肌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仰禽。R本人自食惡果不足惜氮墨,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吐葵。 院中可真熱鬧规揪,春花似錦、人聲如沸温峭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凤藏。三九已至奸忽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揖庄,已是汗流浹背栗菜。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹄梢,地道東北人疙筹。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像禁炒,于是被迫代替她去往敵國和親而咆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359