前言
Fragment在日常開發(fā)中非常常用,但是你有沒有想過它到底是怎樣的一種存在呢?
其實可以簡單地認為它就是一個“控件”,更加具體一點就是“View控制器”。它把自己承載的View展示到容器View中,自身有生命周期,并能添加邏輯控制視圖。
其主要具有以下優(yōu)點:
- 復用, 一個Fragment可以多個Activity使用
- 解耦, 能將業(yè)務邏輯模塊分離
- 靈活, 能夠比較容易適配手機和平板,根據(jù)屏幕的寬度決定Fragment的放置
- 支持回退棧等操作
雖然Fragment有它的優(yōu)點,但是在實際的使用的過程中有很多要理解和注意的細節(jié)。本文主要是帶大家討論一下這些細節(jié),主要從兩個重點切入,一個是Fragment的生命周期,另一個是Activity重創(chuàng)建引發(fā)的問題。閱讀本文需要對Fragment以及Activity重創(chuàng)建有一定了解。
Activity重創(chuàng)建是指由于系統(tǒng)配置改變(屏幕方向,字體大小等)或者內(nèi)存不足 引起對Activity的銷毀與重建
注:本文基于support 26.1.0
support.v4 還是android.app
到目前為止所有的設(shè)備都是api 15以上了,那是不是應該用android.app.Fragment?當然不是
這個其實google的文檔里有說明過,大致有以下兩點原因:
1)Fragment的發(fā)展過程中有bug,support.v4的Fragment可以及時解決一些bug
2)Fragment的發(fā)展過程中增加新特性,support.v4的Fragment可以兼容所有版本。比如Android P增加的LifeCycle特性。
總之就是不斷更新,可以兼容所有版本,當然你的Activity需要使用FragmentActivity的子類配合使用。
關(guān)注Activity重創(chuàng)建對Fragment的影響
在Activity的onCreate創(chuàng)建Fragment要先判斷是否已經(jīng)包含
有些初學者在Activity onCreate方法中動態(tài)創(chuàng)建Fragment時候會這么寫:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_life_cycle);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = new FragmentOne();
transaction.add(R.id.container, fragment,FRAGMENT_ONE_TAG);
transaction.commit();
}
這種寫法是有問題的,因為Activity在重建的時候FragmentManager會根據(jù)之前保存的Fragment相關(guān)的狀態(tài)幫我們重新創(chuàng)建Fragment實例,因此上面的寫法會導致每次多創(chuàng)建一個Fragmnet。因此我們要先檢查一下FragmentManager是否有該Fragment
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_life_cycle);
fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_ONE_TAG);
if (fragment == null) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
fragment = new FragmentOne();
transaction.add(R.id.container, fragment,FRAGMENT_ONE_TAG);
transaction.commit();
}
}
這里不知道你是否會困惑FragmentManager究竟為我們保存了Fragment的哪些狀態(tài)數(shù)據(jù)?這些要被保存的信息保存在FragmentState類中(至于為什么是這個類,不是本文的重點),我們來看一下這個類的字段:
final class FragmentState implements Parcelable {
final String mClassName;
final int mIndex;
final boolean mFromLayout;
final int mFragmentId;
final int mContainerId;
final String mTag;
final boolean mRetainInstance;
final boolean mDetached;
final Bundle mArguments;
final boolean mHidden;
...
}
- mClassName類名,重新創(chuàng)建Fragment實例時候用到的
- mIndex,在Fragment列表里的位置
- mFragmentId,F(xiàn)ragment的id,靜態(tài)創(chuàng)建可以在<fragment>里設(shè)置,動態(tài)創(chuàng)建就是所在的容器的id
- mContainerId,F(xiàn)ragment所在的容器的id
- mTag 標簽
- mRetainInstance,是否在Activity重創(chuàng)建時候保留Fragment實例
- mDetached ,是否對該Fragment執(zhí)行了detach操作
- mArguments,setArguments的參數(shù)
- mHidden,是hide還是show
使用setArguments傳遞參數(shù),而不是構(gòu)造函數(shù)
不知道你有沒有想過,為什么我們在創(chuàng)建Frament實例時候,官方推薦我們使用setArguments來傳遞參數(shù),而不是構(gòu)造函數(shù)。其實看上面FragmentState保存了mArguments,就應該恍然大悟,使用setArguments在Activity重創(chuàng)建時候也會幫我我們保存與恢復參數(shù)值。
注意保存與恢復我們自己的業(yè)務數(shù)據(jù)
沒錯,Activity重創(chuàng)建時候FragmentManager已經(jīng)幫我們保存和恢復了Fragment的實例和部分狀態(tài),但是我們業(yè)務數(shù)據(jù)的保存和恢復還是需要靠我們自己來動手。與Activity類似,我們在Fragment的onSaveInstanceState保存數(shù)據(jù),然后再在onCreate,onCreateView或者onActivityCreated方法中都可以進行恢復。
雖然國內(nèi)很多應用都是固定豎屏的,但是你還要考慮應用在后臺內(nèi)存不足的情況以及其他配置變化引起得Activity重建,雖然可能發(fā)生次數(shù)較少,但是嘛嚴謹?shù)某绦騿T還是盡可能處理這些情況
生命周期
這是一張Fragment的生命周期圖,沒錯,就是這么復雜,這也是剛使用Fragment感覺問題很多的原因。只單純地去搞懂每個生命周期方法的意義和作用不利于我們對整體的把握,下面我們從另外兩個方面更全面地理解這些生命周期的作用和意義。
與Activity生命周期的關(guān)系
在這種情況下Fragment就像是一個牽線木偶,它的所有生命周期都是由Actvity驅(qū)動,這一點你只要看一下FramentActivity生命周期方法就很容易明白。
在Activty的onCreate方法中創(chuàng)建Fragment,生命周期的交叉關(guān)系如下:
LifeCycleActivity: onCreate
FragmentOne: onAttach
FragmentOne: onCreate
FragmentOne: onCreateView
FragmentOne: onActivityCreated
在Actvity的onCreate方法執(zhí)行完后,分別執(zhí)行了Fragment以上的四個生命周期,其實onActivityCreated 的調(diào)用是發(fā)生在FragmentActivity的onStart方法中。另外在Activity重創(chuàng)建時候,F(xiàn)ragment的onAttach和onCreate方法會先于Activity的onCreate方法執(zhí)行。
之后分別執(zhí)行了onStart和onResume方法,可能和想象的順序有所差別,不過也沒什么影響,不同版本的庫表現(xiàn)可能會有所差異。
FragmentOne: onStart
LifeCycleActivity: onStart
LifeCycleActivity: onResume
FragmentOne: onResume
按下Back鍵后:
FragmentOne: onPause
LifeCycleActivity: onPause
FragmentOne: onStop
LifeCycleActivity: onStop
FragmentOne: onDestroyView
FragmentOne: onDestroy
FragmentOne: onDetach
LifeCycleActivity: onDestroy
分別執(zhí)行了Fragment和Activiyt的onPause,onStop方法。然后Fragment銷毀試圖(onDestroyView),銷毀實例(onDestroy),取消與Activity的關(guān)聯(lián)(onDetach),之后執(zhí)行Activity的onDestroy。
總體上這種交叉關(guān)系還是比較對稱的,就像Android中很多生命周期方法調(diào)用順序一樣。
與Fragment管理方法之間的關(guān)系
理解調(diào)用某個管理Fragment的方法之后,F(xiàn)ragment到底執(zhí)行了哪些生命周期方法,主要是能更好區(qū)別這些方法之間的區(qū)別。
add
向containerViewId中添加一個Fragment實例,此時Fragment會執(zhí)行
onAttach-> onCreate-> onCreateView-> onActivityCreated-> onStart ->onResume-
remove
與add操作相反,移除一個Fragment實例,如果移除時候這個fragment沒有被添加到回退棧里,就會銷毀這個Fragment的實例。執(zhí)行下面的生命周期:
onPause-> onStop-> onDestroyView-> onDestroy-> onDetach
如果移除時把這個事物加入回退棧,就好比下面的代碼:FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.remove(fragment); transaction.addToBackStack(null); transaction.commit();Fragment實例就不會被銷毀,只執(zhí)行如下的生命周期方法:
onPause->onStop->onDestroyView
另外多說一點,添加回退棧是指將當前事務添加回退棧,當按下Back鍵時候會做一個和該事務相反的操作。 replace
先移除所有containerViewId中的實例,然后添加一個新的Fragment實例。detach
銷毀Fragment的視圖,但保留實例。
onPause->onStop->onDestroyViewattach
與detach相對,重新創(chuàng)建Fragment視圖。
onCreateView->onActivityCreated->onStart->onResumehide
隱藏Fragment,只是設(shè)為不可見狀態(tài),并不執(zhí)行任何生命周期方法,不銷毀實例,也不銷毀視圖,就好像只是把Fragmnet的視圖設(shè)置為INVISIBLE。此時Fragment的mHidden為true,會引起onHiddenChanged()回調(diào)調(diào)用。show
與hide對應,顯示出Fragment,同樣不執(zhí)行任何生命周期方法。
1)需要注意的是視圖銷毀之后,界面上保留的一些數(shù)據(jù)也就沒有啦。比如用戶在EditText中輸入了數(shù)據(jù),你執(zhí)行remove并把事務添加到回退棧,按下back鍵,這時候EditText中就沒有數(shù)據(jù)啦。如果你希望保留數(shù)據(jù)就用hide。
2)Fragment實例銷毀,視圖自然銷毀。但視圖銷毀,實例不一定銷毀,不用多解釋了吧。
3)一般情況下Fragment實例銷毀才會執(zhí)行onDetach方法,但是還有另外一種情況3就是Fragment實例還在,Activity實例卻銷毀了,也很好理解,你關(guān)聯(lián)的Actvity實例都沒有啦,這種情況其實就是我們下面要說的setRetainInstance.
setRetainInstance
Fragment還有另外一個非常神奇的設(shè)計,就是調(diào)用了setRetainInstance(true)的Fragment在Activity重創(chuàng)建時候不會銷毀Fragment的實例,只是會銷毀視圖并detach,不會執(zhí)行onDestroy。在Activity銷毀重建期間執(zhí)行如下的生命周期方法:
onPause-> onStop ->onDestroyView ->onDetach
onAttach -> onCreateView-> onActivityCreated-> onStart->onResume
但是這種Fragment有什么用呢?在Activity重創(chuàng)建期間,一些簡單的數(shù)據(jù)我們可能通過onSaveInstanceState方法保存就很方便啦,但是有時候會遇見很復雜的數(shù)據(jù),此時我們利用Fragment保留實例的這種特性,創(chuàng)建一個沒有界面的Fragment用來保存數(shù)據(jù)是非常方便的一種做法。比如我們在實現(xiàn)MVVM架構(gòu)的App時候,ViewModel通常就比較復雜,我會創(chuàng)建如下的Fragment類保存ViewModel。
public class ViewModelHolder<VM> extends Fragment {
private VM mViewModel;
public ViewModelHolder() { }
public static <M> ViewModelHolder createContainer(@NonNull M viewModel) {
ViewModelHolder<M> viewModelContainer = new ViewModelHolder<>();
viewModelContainer.setViewModel(viewModel);
return viewModelContainer;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Nullable public VM getViewModel() {
return mViewModel;
}
public void setViewModel(@NonNull VM viewModel) {
mViewModel = viewModel;
}
}
然后通過以下方法去查找FragmentManger中是否有該ViewModelHolder,如果有的話就取到之前保存的數(shù)據(jù)啦。
public static <T> T findViewModel(@NonNull FragmentManager fragmentManager, String viewModelTag) {
ViewModelHolder<T> retainedViewModel =
(ViewModelHolder<T>) fragmentManager
.findFragmentByTag(viewModelTag);
if (retainedViewModel != null && retainedViewModel.getViewModel() != null) {
return retainedViewModel.getViewModel();
}
return null;
}
總之這個特性還是很強大的。
重疊?不存在的
只要你理解并處理好上面所說的問題,同時使用24.0.0以上版本的support庫的話,一般就不會再出現(xiàn)Fragment的重疊問題啦。
24.0.0以下版本bug:
24.0.0之前的support庫有一個bug,就是在FragmentManager保存Fragment實例狀態(tài)的時候,沒有保存mHidden,因此重創(chuàng)建之后Fragment都處于顯示狀態(tài)就造成了重疊。以下是截圖:
所以呢,最好使用24.0.0以上版本的support庫。假如你必須要使用24.0.0以下版本的話,那就自己在Fragment的onSaveInstanceState保存isHidden(),然后再onCreate去顯示或隱藏。
public class BaseFragment extends Fragment {
private static final String IS_HIDDEN = "IS_HIDDEN";
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
boolean isHidden = savedInstanceState.getBoolean(IS_HIDDEN);
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (isHidden) {
ft.hide(this);
} else {
ft.show(this);
}
ft.commit();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(IS_HIDDEN, isHidden());
}
}
注意Fragment是否創(chuàng)建了多個實例
在Activity onCreate中創(chuàng)建實例的話,一定要注意不要重復add。
總之倘若發(fā)生Fragment重疊,要搞清楚容器View里此時有哪幾個Fragment實例,每個實例是顯示還是隱藏,問題就能夠得到解決。
getActivity()
這個我本來不想說,但網(wǎng)上經(jīng)常有人遇到getActivity()為null的問題。這往往都是因為在onAttach之前或者onDetach之后調(diào)用了getActivity,
其實在Fragment在onAttach之后,onDetach之前getActivity都是不會為null的。
因此開發(fā)者要處理好自己的邏輯代碼,避免在不正確的時機調(diào)用getActivity。比如Fragment有一個網(wǎng)絡請求發(fā)送出去,
這時候候Fragment已經(jīng)銷毀了,并執(zhí)行了onDetach,然后網(wǎng)絡請求回調(diào)調(diào)用了getActivity。這其實就是內(nèi)存泄漏了,應該
在Fragment onDestroy的時候就打斷網(wǎng)絡請求回調(diào)引用鏈或者使用弱引用。
而有些同學會在onAttach方法中自己再保存一個Activity的引用,這種做法在我看來是錯誤的。
DialogFragment
DialogFragment是繼承自Fragment,使用它來創(chuàng)建對話框明顯的好處就是Activity重創(chuàng)建時候?qū)υ捒蚰芟馞ragment一樣被恢復。如果你對它不是很熟悉的話,推薦鴻神的
Android 官方推薦 : DialogFragment 創(chuàng)建對話框。
當Fragment遇上ViewPager
Fragment和ViewPager組合使用,也有非常多有意思的細節(jié)。限于篇幅原因,請查閱當Fragment遇上ViewPager