在各種Android項目中,我們不可避免要使用到Fragment,但很多地方其實我們只是習慣性或copy代碼來使用,很多地方并沒有深入去了解,今天就通過這篇文章整理和回顧一下關于Fragment的種種。而在自己總結的過程中,我也發現自己在很多細節方面有一些知識上的不足和理解上的錯誤,這也會讓我對整理的知識掌握得更全面。
Fragment的生命周期
生命周期自然是首先要弄清楚的,先上我們最經常見到的Android官網關于Fragment的生命周期圖:
關于Fragment最基本的生命周期大家應該都很熟悉,但有幾點需要詳細說一下:
1. 關于Fragment的回收
- 仔細看上圖中的英文提示,會發現在Fragment的onDestroyView()方法之后有兩個剪頭指向,一個是直接去執行onDestroy()方法,另一個是重新去走onCreateView()方法。造成這兩種情況的原因是因為FragmentManager對Fragment的不同管理方式。用一個我們最常用到的場景來說明:
ViewPager搭配Fragment來使用時,系統為我們提供了兩個適用的adapter:FragmentPagerAdapter和FragmentStatePagerAdapter,這兩個adapter最大的不同之處就是對Fragment的回收管理。在FragmentPagerAdapter的instantiateItem()和destroyItem方法中,對多個Fragment展示和回收的處理主要是通過FragmentTransaction的attach和detach方法來處理;而在FragmentStatePagerAdapter中,是通過通過FragmentTransaction的add和remove方法來處理。后者的Fragment在不可見時執行完onDestroyView()后直接去執行onDestroy()把當前Fragment完全銷毀;而前者的Fragment在執行完onDestroyView()后則不再執行,而會在下一次這個Fragment重新可見時,去通過onCreateView()來重新創建視圖,也就是說Fragment并沒有被完全銷毀而只是被回收了View而已。
這一點在后面關于ViewPager使用場景相關的文章中我會再在詳細說到。
2. Fragment和Activity生命周期的聯系
- 自然先看官方給出的聯系圖:
上圖雖然很清晰,但Activity和Fragment生命周期每個階段更細致的順序并看不出來。這點只有通過手動跑一下測試代碼來看了
上面是我通過一個簡單的測試代碼來打印的生命周期log,啟動MainActivity,TestFragment顯示MainActivity中。值得注意的,除了onResume方法是Activity先執行而Fragment后執行外,其他階段的生命周期方法都是Fragment先執行之后,Activity再執行的。
3. Fragment的onSaveInstanceState方法**
onSaveInstanceState的調用時機Fragment通Activity是一樣的, 都是在當前界面進入可被系統回收狀態時就會被調用。看一下啟動MainActivity后按home鍵回到桌面時,onSaveInstanceState的調用情況:
所以,當我們需要保存Fragment相關的狀態時,可以通過這個方法。需要注意的一點是,Fragment本身并沒有通Activity一致的onRestoreInstanceState方法,所以如官方文檔所說
您可以在Fragment的onSaveInstanceState()回調期間保存狀態,并可在onCreate()、onCreateView() 或 onActivityCreated() 期間恢復狀態。
4. 關于Fragment的setUserVisibleHint方法**
這個方法嚴格來說不屬于Fragment生命周期的范疇,但有人把它比作是Fragment真正的onResume和onStop方法,主要是因為配合這個方法可以在使用ViewPager+Fragment時實現懶加載,因為Fragment的onResume方法與Activity的onResume方法是一致的,所以無法通過onResume方法來判斷Fragment是否可見,反而可以通過setUserVisibleHint來準確判斷(關于ViewPager的懶加載后續的文章中也會詳細講到),當然如果項目中有用到友盟統計,也可以通過該方法更加準確的上報Fragment的相關數據。
說setUserVisibleHint不屬于Fragment生命周期的范疇主要是因為它并不會被系統主動來回調。原先我也認為只要Fragment的可見性發生變化就會回調它,自己用代碼打印時才發現并非如此。如果Activity中有一個Fragment,無論是進入另一個Activity,還是按home鍵回到桌面,setUserVisibleHint方法都不會調用。Google一番外加看源碼,才明白原來這個方法是需要我們主動調用來告知系統當前Fragment的可見性,源碼注釋這樣說:
/* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* <p>An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.</p>
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = !isVisibleToUser;
}
系統為我們提供的配合ViewPager+Fragment的適配器FragmentPagerAdapter和FragmentStatePagerAdapter中,也是在ViewPager的fragment item進行初始化和切換時主動調用了該方法。
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
// 此處主動調用了setUserVisibleHint方法
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
5. 其他
Activity 生命周期與Fragment生命周期之間的最顯著差異在于它們在其各自返回棧中的存儲方式。 默認情況下,Activity 停止時會被放入由系統管理的 Activity 返回棧(以便用戶通過“返回”按鈕回退到Activity,任務和返回棧對此做了闡述)。不過,僅當您在刪除Fragment的事務執行期間通過調用 addToBackStack() 顯式請求保存實例時,系統才會將Fragment放入由宿主 Activity 管理的返回棧。
在其他方面,管理Fragment生命周期與管理 Activity 生命周期非常相似。 因此,管理 Activity 生命周期的做法同樣適用于Fragment。
注意:如需 Fragment 內的某個 Context 對象,可以調用 getActivity()。但要注意,請僅在Fragment附加到 Activity 時調用 getActivity()。如果Fragment尚未附加,或在其生命周期結束期間分離,則 getActivity() 將返回 null。
Fragment的使用
Fragment必須始終嵌套在Activity中,其生命周期直接受宿主 Activity 生命周期的影響。 例如,當 Activity 暫停時,其中的所有的Fragment也會暫停;當 Activity 被銷毀時,所有的Fragment也會被銷毀。 不過,當 Activity 正在運行(處于onResume狀態)時,您可以獨立操縱每個Fragment,如添加或移除它們。 當您執行此類Fragment事務時,您也可以將其添加到由 Activity 管理的返回棧—Activity 中的每個返回棧條目都是一條已發生Fragment事務的記錄。 返回棧讓用戶可以通過按“返回”按鈕撤消Fragment事務(后退)。
當您將Fragment作為 Activity 布局的一部分添加時,它存在于 Activity 視圖層次結構的某個 ViewGroup 內部,并且Fragment會定義其自己的視圖布局。您可以通過在 Activity 的布局文件中聲明Fragment,將其作為 <fragment>
元素插入您的 Activity 布局中,或者通過將其添加到某個現有 ViewGroup,利用應用代碼進行插入。不過,Fragment并非必須成為 Activity 布局的一部分;您還可以將沒有自己 UI 的Fragment用作 Activity 的不可見工作線程。
1. 創建Fragment的視圖
要想為Fragment提供布局,就必須實現onCreateView()回調方法,Android系統會在Fragment需要繪制其布局時調用該方法。對此方法的實現返回的View必須是片段布局的根視圖。
要想從onCreateView()返回布局,可以通過xml中定義的布局資源來擴展布局。為此,onCreateView()專門提供了一個LayoutInflater對象。
例如,以下這個Fragment子類從 example_fragment.xml文件加載布局:
public static class ExampleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
傳遞至onCreateView()的container參數是你的Fragment布局將插入到的父ViewGroup(來自Activity的布局)。savedInstanceState是在恢復Fragment時,提供上一Fragment實例相關數據的Bundle。
inflate()方法帶有三個參數:
- 你想要擴展的布局的資源ID;
- 將作為擴展布局父項的ViewGroup。傳遞 container
對系統向擴展布局的根視圖(由其所屬的父視圖指定)應用布局參數具有重要意義; - 指示是否應該在擴展期間將擴展布局附加至 ViewGroup
(第二個參數)的布爾值。(在本例中,其值為 false,因為系統已經將擴展布局插入container
—傳遞 true 值會在最終布局中創建一個多余的視圖組。)
關于inflate常用的三個方法的總結:
- 調用LayoutInflater.inflate方法,并且將root參數設置為null,就等于忽略了xml布局文件中的layout_×參數(而如gravity、background等這樣的非layout參數則依然會生效),并返回布局文件對應的忽略layout_×參數的view對象;
- 如果root不為null的話,就根據root會為xml布局文件生成一個LayoutParam對象,如果attachToRoot參數為false,那么就將這個param對象設置給這個布局文件的View;
- 如果root不為null,并且attachRoot=true,那么就會根據root生成一個布局文件View的LayoutParam對象,并且將這個View添加到root中去,并返回這個root的View。
看inflate的源碼可以看出對三種不同情況的處理:
// resource為inflate方法中傳入的xml布局資源參數
final XmlResourceParser parser = res.getLayout(resource);
……
final AttributeSet attrs = Xml.asAttributeSet(parser);
……
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
……
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
2. 向Activity中添加Fragment
a. 在 Activity 的布局文件內聲明片段
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/list"
android:name="com.example.news.ArticleListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/viewer"
android:name="com.example.news.ArticleReaderFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
</LinearLayout>
Fragment的android:name屬性指定要在布局中實例化的Fragment。
當系統創建此 Activity 布局時,會實例化在布局中指定的每個Fragment,并為每個Fragment調用 onCreateView() 方法,以檢索每個Fragment的布局。系統會直接插入Fragment返回的 View
來替代 fragment 元素。
使用這種方式時,fragment元素的id是必須設置的,否則會crash
每個Fragment都需要一個唯一的標識符,重啟 Activity 時,系統可以使用該標識符來恢復Fragment(您也可以使用該標識符來捕獲Fragment以執行某些事務,如將其刪除)。 可以通過三種方式為Fragment提供 ID:
- 為 android:id屬性提供唯一 ID
- 為 android:tag屬性提供唯一字符串
- 如果您未給以上兩個屬性提供值,系統會使用容器視圖的 ID
這里我測試發現,這樣的顯示Fragment時,inflate的xml中根ViewGroup的layout_×參數會被忽略掉:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="我是其他Fragment" />
</LinearLayout>
將上述布局的Fragment添加到一個Activity的布局中:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/second_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/otherFragment"
android:name="holenzhou.com.aboutfragment.view.OtherFragment"
android:layout_width="match_parent"
android:layout_weight="match_parent"/>
</LinearLayout>
會發現寬和高的參數被忽略掉了,而是以fragment元素的layout參數為準了。
Debug發現此時OtherFragment的onCreateView()方法中,傳進來的container參數為null,很奇怪,Google了一番,發現stackoverflow上面也有類似的問答,但都沒有說清楚是為什么。
b. 在代碼中動態添加Fragment到某個現有的ViewGroup
您可以在 Activity 運行期間隨時將Fragment添加到 Activity 布局中。您只需指定要將Fragment放入哪個 ViewGroup。
要想在您的 Activity 中執行Fragment事務(如添加、刪除或替換Fragment),您必須使用 FragmentTransaction中的 API。您可以像下面這樣從 Activity 獲取一個 FragmentTransaction 實例:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
然后,您可以使用 add()方法添加一個Fragment,指定要添加的Fragment以及將其插入哪個視圖。例如
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
傳遞到 add() 的第一個參數是 ViewGroup ,即應該放置Fragment的位置,由資源 ID 指定,第二個參數是要添加的Fragment。
一旦您通過 FragmentTransaction 做出了更改,就必須調用 commit() 以使更改生效。
Fragment可以沒有UI,用作 Activity 的不可見工作線程。添加此類型的Fragment,使用add(Fragment, String)從 Activity 添加Fragment(為Fragment提供一個唯一的字符串“標記”,而不是視圖 ID)。這會添加Fragment,但由于它并不與 Activity 布局中的視圖關聯,因此不會收到對 onCreateView() 的調用。因此,您不需要實現該方法。
3. 管理Fragment
通過使用FragmentManager來管理Activity中的Fragment,可以執行的操作包括:
- 通過 findFragmentById()(對于在 Activity 布局中提供 UI 的Fragment)或 findFragmentByTag()(對于提供或不提供 UI 的Fragment)獲取 Activity 中存在的Fragment
- 通過 popBackStack()(模擬用戶發出的 Back 命令)將片段從返回棧中彈出
- 通過 addOnBackStackChangedListener() 注冊一個偵聽返回棧變化的偵聽器
4. 執行Fragment事務
在 Activity 中使用Fragment的一大優點是,可以根據用戶行為通過它們執行添加、刪除、替換以及其他操作。 您提交給 Activity 的每組更改都稱為事務,您可以使用 FragmentTransaction 中的 API 來執行一項事務。您也可以將每個事務保存到由 Activity 管理的返回棧內,從而讓用戶能夠回退Fragment更改(類似于回退 Activity)。
每個事務都是您想要同時執行的一組更改。您可以使用 add(), replace(), remove() 等方法為給定事務設置您想要執行的所有更改。然后,要想將事務應用到 Activity,您必須調用 commit() 。
不過,在您調用 commit() 之前,您可能想調用 addToBackStack(),以將事務添加到Fragment事務返回棧。 該返回棧由 Activity 管理,允許用戶通過按“返回”按鈕返回上一Fragment狀態。
例如,以下示例說明了如何將一個Fragment替換成另一個Fragment,以及如何在返回棧中保留先前狀態:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container,newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
在上例中,newFragment會替換目前在 R.id.fragment_container ID 所標識的布局容器中的任何Fragment(如有)。通過調用 addToBackStack() 可將替換事務保存到返回棧,以便用戶能夠通過按“返回”按鈕撤消事務并回退到上一Fragment。
如果您向事務添加了多個更改(如又一個 add() 或 remove()),并且調用了 addToBackStack(),則在調用 commit() 前應用的所有更改都將作為單一事務添加到返回棧,并且“返回”按鈕會將它們一并撤消。
向FragmentTransaction添加更改的順序無關緊要,不過:
- 必須最后調用 commit();
- 如果您要向同一容器添加多個Fragment,則您添加Fragment的順序將決定它們在視圖層次結構中的出現順序。
如果您沒有在執行刪除Fragment的事務時調用 addToBackStack(),則事務提交時該Fragment會被銷毀,用戶將無法回退到該Fragment。 不過,如果您在刪除Fragment時調用了 addToBackStack(),則系統會停止該Fragment,并在用戶回退時將其恢復。
提示:對于每個Fragment事務,您都可以通過在提交前調用 setTransition() 來應用過渡動畫。
調用 commit() 不會立即執行事務,而是在 Activity 的 UI 線程(主線程)可以執行該操作時再安排其在線程上運行。不過,如有必要,您也可以從 UI 線程調用 executePendingTransactions() 以立即執行 commit() 提交的事務。通常不必這樣做,除非其他線程中的作業依賴該事務。
注意:您只能在 Activity保存其狀態(用戶離開 Activity)之前使用 commit() 提交事務。如果您試圖在該時間點后提交,則會引發異常。 這是因為如需恢復 Activity,則提交后的狀態可能會丟失。 對于丟失提交無關緊要的情況,請使用 commitAllowingStateLoss()。
5. 與Activity通信
- Fragment通過getActivity()訪問Activity實例,并輕松地執行在Activity布局中查找視圖等任務;
View listView = getActivity().findViewById(R.id.list);
2.Activity中通過findFragmentById()或者findFragmentByTag(),通過從FragmentManager獲得對Fragment的引用來調用Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
6. 創建對 Activity 的事件回調
為了在Fragment和Activity間共享數據,可以在Fragment內定義一個回調接口。并要求宿主 Activity 實現它。 當 Activity 通過該接口收到回調時,可以根據需要與布局中的其他Fragment共享這些信息。(比如左邊FragmentA中顯示文章列表,右邊FragmentB中顯示相對應文章內容的場景)
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
在onAttach()回調中強轉宿主Activity為指定接口,Activity實現接口時,mListener成員會保留對 Activity 的OnArticleSelectedListener 實現的引用,以便FragmentA 可以通過調用 OnArticleSelectedListener 定義的方法與 Activity 共享事件。
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + "must implement OnArticleSelectedListener");
}
}
...
}