本文的主要目的介紹的是當(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 {
// 非本文討論的情況
}
// 略........
}
- 為減少不必要的代碼吴攒,文章中的
fm
、FM
均指代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ù)用的好處:
- 避免顯示錯亂
- 避免重復(fù)添加
- 避免多余的內(nèi)存占用
- 優(yōu)化界面啟動速度
- ........
所以復(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ù)是我們在進行 add
或 replace
操作的時候指定的儡羔。
提示:
- tag 是可以重復(fù)的,因為該參數(shù)的之只是 Fragment 的一個成員變量璧诵,只是我們無法訪問(訪問權(quán)限 default)汰蜘。
- 該方法總是返回 FragmentManager 中和該 tag 一致的最后一個 Fragment。也就是說如果有多個 Fragment 對象使用了同一個 tag 那么最后一個被添加的會被返回腮猖,所以不要為不同的 Fragment 對象指定相同的 tag鉴扫。
- 不要為同一個 Fragment 實例對象指定在不同的操作中指定不同的 tag赞枕,不然會拋出異常澈缺,當(dāng)然這種情況一般是發(fā)生在重復(fù)添加的情況下
六坪创、與 ViewPager 配合時不要試圖使用 FM.findFragmentByTag
上面的 findFragmentById
和 findFragmentByTag
在使用的時候其實都是有一些隱藏限制的:
- 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é)合圖片解析
上圖只是給出了我們已經(jīng)知道的,未知的 Fragment 沒有表示出來州疾,但不代表不存在
以 圖中 Fragment A 為例,其他的同理
- 當(dāng)存儲狀態(tài)的時候我們通過putFragment 記錄下 FragmentA 的 mIndex, 使用的key 為字符串 "fragment:A"
- 當(dāng)我們需要查找 A 的時候严蓖,先根據(jù) 字符串 "fragment:A"(putFragment時使用的值) 去 bundle 中查出我們在 fragmentManager 銷毀前記錄的 mIndex = 5
- 通過 mActivie 中得到 key = 5 的Fragment對象 即:Fragment A
- 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 無法訪問到所以才需要 getFragment 和 putFragment薄嫡。
注意事項:
- getFragment 和 putFragment 必須成對使用。
- 在調(diào)用 putFragment 方法之前先保證該 fragment 是否已經(jīng)添加到 FragmentManager 了(即fragment.mIndex >= 0)颗胡,不然從源碼可以得知會拋出異常毫深。
七、總結(jié)
- 在寫 Activity 和 Fragment 的代碼時區(qū)分區(qū)分新建和恢復(fù)毒姨,在恢復(fù)的情況下先查找 Fragment哑蔫,找不到再創(chuàng)建實例對象
- FM.getFragment 適合多個 Fragment 共用一個 ViewGroup 同時還無法為Fragment指定Tag的情況(如ViewPager)
- FM.findFragmentById 適合一個 ViewGroup 對應(yīng) 一個 Fragment 的情況
- FM.findFragmentByTag 適合大多數(shù)情況,但需要在 add/replace 的時候為每個 Fragment 指定不同 tag
- 當(dāng)有多個 Fragment 對象具有相同的 tag 時弧呐,通過 findFragmentByTag 得到的是最后被添加的 Fragment
- 當(dāng)有多個 Fragment 對象共用同意個ViewGroup時鸳址,通過 findFragmentById 得到的是最后被添加的 Fragment
- putFragment 使用時先判斷 Fragment 是否已經(jīng)添加到 FragmentManager
最后附上一張圖告訴你如何選擇合適的方法來查找Fragment
我最近剛剛開通了微信公眾號(怪盜kidou),歡迎關(guān)注