當我們談Fragment時,我們談些什么之一

在各種Android項目中,我們不可避免要使用到Fragment,但很多地方其實我們只是習慣性或copy代碼來使用,很多地方并沒有深入去了解,今天就通過這篇文章整理和回顧一下關于Fragment的種種。而在自己總結的過程中,我也發現自己在很多細節方面有一些知識上的不足和理解上的錯誤,這也會讓我對整理的知識掌握得更全面。

Fragment的生命周期

生命周期自然是首先要弄清楚的,先上我們最經常見到的Android官網關于Fragment的生命周期圖:

Fragment的生命周期.png

關于Fragment最基本的生命周期大家應該都很熟悉,但有幾點需要詳細說一下:

1. 關于Fragment的回收

  • 仔細看上圖中的英文提示,會發現在Fragment的onDestroyView()方法之后有兩個剪頭指向,一個是直接去執行onDestroy()方法,另一個是重新去走onCreateView()方法。造成這兩種情況的原因是因為FragmentManager對Fragment的不同管理方式。用一個我們最常用到的場景來說明:

ViewPager搭配Fragment來使用時,系統為我們提供了兩個適用的adapter:FragmentPagerAdapter和FragmentStatePagerAdapter,這兩個adapter最大的不同之處就是對Fragment的回收管理。在FragmentPagerAdapter的instantiateItem()destroyItem方法中,對多個Fragment展示和回收的處理主要是通過FragmentTransaction的attachdetach方法來處理;而在FragmentStatePagerAdapter中,是通過通過FragmentTransaction的addremove方法來處理。后者的Fragment在不可見時執行完onDestroyView()后直接去執行onDestroy()把當前Fragment完全銷毀;而前者的Fragment在執行完onDestroyView()后則不再執行,而會在下一次這個Fragment重新可見時,去通過onCreateView()來重新創建視圖,也就是說Fragment并沒有被完全銷毀而只是被回收了View而已。

這一點在后面關于ViewPager使用場景相關的文章中我會再在詳細說到。

2. Fragment和Activity生命周期的聯系

  • 自然先看官方給出的聯系圖:

上圖雖然很清晰,但Activity和Fragment生命周期每個階段更細致的順序并看不出來。這點只有通過手動跑一下測試代碼來看了

Fragment啟動
Fragment銷毀

上面是我通過一個簡單的測試代碼來打印的生命周期log,啟動MainActivity,TestFragment顯示MainActivity中。值得注意的,除了onResume方法是Activity先執行而Fragment后執行外,其他階段的生命周期方法都是Fragment先執行之后,Activity再執行的。

3. Fragment的onSaveInstanceState方法**

onSaveInstanceState的調用時機Fragment通Activity是一樣的, 都是在當前界面進入可被系統回收狀態時就會被調用。看一下啟動MainActivity后按home鍵回到桌面時,onSaveInstanceState的調用情況:

onSaveInstanceState的調用

所以,當我們需要保存Fragment相關的狀態時,可以通過這個方法。需要注意的一點是,Fragment本身并沒有通Activity一致的onRestoreInstanceState方法,所以如官方文檔所說

您可以在Fragment的onSaveInstanceState()回調期間保存狀態,并可在onCreate()、onCreateView() 或 onActivityCreated() 期間恢復狀態。



4. 關于Fragment的setUserVisibleHint方法**

這個方法嚴格來說不屬于Fragment生命周期的范疇,但有人把它比作是Fragment真正的onResumeonStop方法,主要是因為配合這個方法可以在使用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通信

  1. 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");
        }
    }
    ...
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Fragment 表示 Activity中的行為或用戶界面部分。您可以將多個片段組合在一個 Activity 中來...
    鹿小純0831閱讀 408評論 0 0
  • 一個Fragment看起來就是一個和Activity一樣的用戶界面。你可以結合多個Fragments到一個acti...
    kaiviak閱讀 2,286評論 0 8
  • 片段 Fragment表示 Activity中的行為或用戶界面部分。您可以將多個片段組合在一個 Activity ...
    岳小川閱讀 830評論 0 3
  • Fragment表示Activity中的行為或用戶界面部分,我們可以將多個Fragment組合在一個Activit...
    wind_sky閱讀 346評論 0 0
  • 萬叢百花齊開放 巍峨信仰疾呼喚 革命情懷始不渝 井岡薪火代代傳
    一根不屈的筋閱讀 442評論 6 7