Android Fragment 的使用,一些你不可不知的注意事項

Fragment,俗稱碎片,自 Android 3.0 開始被引進並大量使用。然而就是這樣耳熟能詳的一個東西,在開發中我們還是會遇見各種各樣的問題,層出不窮。所以,是時候總結一波了。

Fragment 簡介


作爲 Activity 界面的一部分,Fragment 的存在必須依附於 Activity,並且與 Activity 一樣,擁有自己的生命週期,同時處理用戶的交互動作。同一個 Activity 可以有一個或多個 Fragment 作爲界面內容,並且可以動態添加、刪除 Fragment,靈活控制 UI 內容,也可以用來解決部分屏幕適配問題。

另外,support v4 包中也提供了 Fragment,兼容 Android 3.0 之前的系統(當然,現在 3.0 之前的系統在市場上已經很少見了,可以不予考慮),使用兼容包需要注意兩點:

  • Activity 必須繼承自 FragmentActivity;

  • 使用 getSupportFragmentManager() 方法獲取 FragmentManager 對象;

生命週期


作爲宿主 Activity 的一部分,Activity 擁有的大部分生命週期函數在 Fragment 中同樣存在,並與 Activity 保持同步。同時,作爲一個特殊情況的存在,Fragment 也有一些自己的生命週期函數,如 onAttach()、onCreateView() 等。

至於 Activity 與 Fragment 之間生命週期函數的對應同步關係,來自 GitHub 的 xxv/android-lifecycle 項目用了一幅圖完美地予以展示:

關於 Fragment 各個生命週期函數的意義,這裏就不一一敘述,可以參考官網介紹:Fragment Lifecycle

創建實例


像普通的類一樣,Fragment 擁有自己的構造函數,於是我們可以像下面這樣在 Activity 中創建 Fragment 實例:

1
MainFragment mainFragment = new MainFragment();

如果需要在創建 Fragment 實例時傳遞參數進行初始化的話,可以創建一個帶參數的構造函數,並初始化 Fragment 成員變量等。這樣做,看似沒有問題,但在一些特殊狀況下還是有問題的。

我們知道,Activity 在一些特殊狀況下會發生 destroy 並重新 create 的情形,比如屏幕旋轉、內存吃緊時;對應的,依附於 Activity 存在的 Fragment 也會發生類似的狀況。而一旦重新 create 時,Fragment 便會調用默認的無參構造函數,導致無法執行有參構造函數進行初始化工作。

好在 Fragment 提供了相應的 API 幫助我們解決這個問題。利用 bundle 傳遞數據,參考代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static OneFragment newInstance(int args){
    OneFragment oneFragment = new OneFragment();

    Bundle bundle = new Bundle();
    bundle.putInt("someArgs", args);

    oneFragment.setArguments(bundle);
    return oneFragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Bundle bundle = getArguments();
    int args = bundle.getInt("someArgs");
}

嵌入方式


Activity 嵌入 Fragment 分爲佈局靜態嵌入和代碼動態嵌入兩種。前者在 Activity 的 Layout 佈局中使用 <fragment> 標籤嵌入指定 Fragment,如:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.yifeng.samples.OneFragment"/>

</LinearLayout>

後者在 Activity 的 Java 代碼中藉助管理器類 FragmentManager 和 事務類 FragmentTransaction 提供的 replace() 方法替換 Activity 的 Layout 中的相應容器佈局,如:

1
2
3
4
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fl_content, OneFragment.newInstance());
ft.commit();

這兩種嵌入方式對應的 Fragment 生命週期略有不同,從生命週期圖中可以看出。相比佈局靜態嵌入方式,代碼動態嵌入方式更爲常用,畢竟後者能夠實現靈活控制多個 Fragment,動態改變 Activity 中的內容。

getChildFragmentManager()


像上面這樣,在 Activity 嵌入 Fragment 時,需要使用 FragmentManager,通過 Activity 提供的 getFragmentManager() 方法即可獲取,用於管理 Activity 裏面嵌入的所有一級 Fragment。

然而有時候,我們會在 Fragment 裏面繼續嵌套二級甚至三級 Fragment,即 Activity 嵌套多級 Fragment。此時在 Fragment 裏管理子 Fragment 時,也需要使用到 FragmentManager。但是一定要使用 getChildFragmentManager() 方法獲取 FragmentManager 對象!

從官方文檔註釋上也可以看出這兩個方法獲取到的 FragmentManager 對象的區別:

Activity:getFragmentManager()

Return the FragmentManager for interacting with fragments associated with this activity.

Fragment:getChildFragmentManager()

Return a private FragmentManager for placing and managing Fragments inside of this Fragment.

FragmentTransaction


Fragment 的動態添加、刪除等操作都需要藉助於 FragmentTransaction 類來完成,比如上面提到的 replace() 操作。FragmentTransaction 提供有很多方法供開發人員操作 Activity 裏面的 Fragment,具體可以參考官網介紹:FragmentTransaction Public methods,這裏介紹幾個常用的關鍵方法:

  • add() 系列:添加 Fragment 到 Activity 界面中;

  • remove():移除 Activity 中的指定 Fragment;

  • replace() 系列:通過內部調用 remove() 和 add() 完成 Fragment 的修改;

  • hide() 和 show():隱藏和顯示 Activity 中的 Fragment;

  • addToBackStack():添加當前事務到回退棧中,即當按下返回鍵時,界面迴歸到當前事物狀態;

  • commit():提交事務,所有通過上述方法對 Fragment 的改動都必須通過調用 commit() 方法完成提交;

注意:動態切換顯示 Activity 中的多個 Fragment 時,可以通過 replace() 實現,也可以 hide() 和 show() 方法實現。事實上,我們更傾向於使用後者,因爲 replace() 方法不會保留 Fragment 的狀態,也就是說諸如 EditText 內容輸入等用戶操作在 remove() 時會消失。當然,如果你不想保留用戶操作的話,可以選擇前者,視情況而定。

BackStack(回退棧)


通過 addToBackStack() 保存當前事務,當用戶按下返回鍵時,如果回退棧中保存有之前的事務,便會執行事務回退,而不是 finish 掉當前 Activity。

舉個例子,比如 App 中有一個新用戶註冊功能,包括設置用戶名、密碼、手機號等等流程,設計師在 UI 設計上將每個流程單獨設計成一個界面,引導用戶一步步操作。作爲開發人員,如果將每一個完善信息的流程單獨設置成一個 Activity 的話操作起來就比較繁瑣,並且也不易於應用裏的邏輯處理,而如果使用 Fragment 並結合回退棧的話,就非常合適了。

將每一個設置的流程寫成一個 Fragment,通過狀態控制顯示不同的 Fragment,並利用回退棧實現返回上一步操作的功能。比如從 FirstStepFragment 進入 SecondStepFragment 時,比如可以在 LoginActivity.java 中這樣操作:

1
2
3
4
5
6
7
8
9
10
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(firstStepFragment);
if (secondStepFragment==null){
    ft.add(R.id.fl_content, secondStepFragment);
}else {
    ft.show(secondStepFragment);
}
ft.addToBackStack(null);
ft.commit();

注意:這裏使用了 hide() 方法,而不是 replace() 方法,因爲我們當然希望用戶返回上一步操作時,之前設置的內容不會消失。

通信方式


通常,Fragment 與 Activity 通信存在三種情形:Activity 操作內嵌的 Fragment,Fragment 操作宿主 Activity,Fragment 操作同屬 Activity中的其他 Fragment。

由於 Activity 持有所有內嵌的 Fragment 對象實例(創建實例時保存的 Fragment 對象,或者通過 FragmentManager 類提供的 findFragmentById() 和 findFragmentByTag() 方法也能獲取到 Fragment 對象),所以可以直接操作 Fragment;Fragment 通過 getActivity() 方法可以獲取到宿主 Activity 對象(強制轉換類型即可),進而可以操作宿主 Activity;那麼很自然的,獲取到宿主 Activity 對象的 Fragment 便可以操作其他 Fragment 對象。

雖然上述操作已經能夠解決 Activity 與 Fragment 的通信問題,但會造成代碼邏輯紊亂的結果,極度不符合這一編程思想:高內聚,低耦合。Fragment 做好自己的事情即可,所有涉及到 Fragment 之間的控制顯示等操作,都應交由宿主 Activity 來統一管理。

所以我們強烈推薦,使用對外開放接口的形式將 Fragment 的一些對外操作傳遞給宿主 Activity。具體實現方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OneFragment extends Fragment implements View.OnClickListener{

    public interface IOneFragmentClickListener{
        void onOneFragmentClick();
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View contentView = inflater.inflate(R.layout.fragment_one, null);
        contentView.findViewById(R.id.edt_one).setOnClickListener(this);
        return contentView;
    }

    @Override
    public void onClick(View v) {
        if (getActivity() instanceof IOneFragmentClickListener){
             ((IOneFragmentClickListener) getActivity()).onOneFragmentClick();
        }
    }

}

只要在宿主 Activity 實現 Fragment 定義的對外接口 IOneFragmentClickListener,便可以實現 Fragment 調用 Activity 的功能。

當然,你可以這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OneFragment extends Fragment implements View.OnClickListener{
    
    private IOneFragmentClickListener clickListener;

    public interface IOneFragmentClickListener{
        void onOneFragmentClick();
    }

    public void setClickListener(IOneFragmentClickListener clickListener) {
        this.clickListener = clickListener;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View contentView = inflater.inflate(R.layout.fragment_one, null);
        contentView.findViewById(R.id.edt_one).setOnClickListener(this);
        return contentView;
    }

    @Override
    public void onClick(View v) {
        clickListener.onOneFragmentClick();
    }

}

原理是一樣的,只是相比第一種方式,需要在宿主 Activity 中額外添加一步監聽設置:

1
oneFragment.setClickListener(this);

getActivity() 引用問題


使用中,經常會在 Fragment 中通過 getActivity() 獲取到宿主 Activity 對象,但稍有不慎便會引發下面這兩個問題:

第一個, Activity 的實例銷燬問題。比如,Fragment 中存在類似網絡請求之類的異步耗時任務,當該任務執行完畢回調 Fragment 的方法並用到宿主 Activity 對象時,很有可能宿主 Activity 對象已經銷燬,從而引發 NullPointException 等異常,甚至造成程序崩潰。所以,異步回調時需要注意添加空值等判斷(譬如:fragment.isAdd(),getActivity()!=null 等),或者在 Fragment 創建實例時就通過getActivity().getApplicationContext() 方法保存整個應用的上下文對象,再來使用;

第二個,內存泄漏問題。如果 Fragment 持有宿主 Activity 的引用,會導致宿主 Activity 無法回收,造成內存泄漏。所以,如果可以的話,儘量不要在 Fragment 中持有宿主 Activity 的引用。

爲了解決 Context 上下文引用的問題,Fragment 提供了一個 onAttach(context) 方法,在此方法中我們可以獲取到 Context 對象,如:

1
2
3
4
5
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    this.context = context;
}

Fragment 重疊問題


前面我們介紹 Fragment 初始化時提到 Activity 銷燬重建的問題,試想一下,當 Activity 重新執行 onCreate() 方法時,是不是會再次執行 Fragment 的創建和顯示等操作呢?而之前已經存在的 Fragment 實例也會銷燬再次創建,這不就與 Activity 中 onCreate() 方法裏面第二次創建的 Fragment 同時顯示從而發生 UI 重疊的問題了嗎?

根據經驗,通常我們會在 AndroidManifest 裏將 Activity 設置爲橫屏模式,所以不會由於屏幕旋轉導致這種問題的出現。一種比較多的出現方式是,應用長時間處於後臺,但由於設備內存吃緊,導致 Activity 被銷燬,而當用戶再次打開應用時便會發生 Fragment 重疊的問題。但是這種問題在開發階段由於應用的頻繁使用導致我們很難遇見,但確確實實存在着。所以開發過程中,一定要注意這類問題。

知道問題的根源所在之後,對應的解決方案也就有啦。就是在 Activity 中創建 Fragment 實例時,添加一個判斷即可,處理方式有三種:

第一種方式,在 Activity 提供的 onAttachFragment() 方法中處理:

1
2
3
4
5
6
7
@Override
public void onAttachFragment(Fragment fragment) {
    super.onAttachFragment(fragment);
    if (fragment instanceof  OneFragment){
        oneFragment = (OneFragment) fragment;
    }
}

第二種方式,在創建 Fragment 前添加判斷,判斷是否已經存在:

1
2
3
4
5
6
7
Fragment tempFragment = getSupportFragmentManager().findFragmentByTag("OneFragment");
if (tempFragment==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) tempFragment;
}

第三種方式,更爲簡單,直接利用 savedInstanceState 判斷即可:

1
2
3
4
5
6
if (savedInstanceState==null) {
    oneFragment = OneFragment.newInstance();
    ft.add(R.id.fl_content, oneFragment, "OneFragment");
}else {
    oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("OneFragment");
}

onActivityResult()


Fragment 類提供有 startActivityForResult() 方法用於 Activity 間的頁面跳轉和數據回傳,其實內部也是調用 Activity 的對應方法。但是在頁面返回時需要注意 Fragment 沒有提供 setResult() 方法,可以通過宿主 Activity 實現。

舉個例子,在 ActivityA 中的 FragmentA 裏面調用 startActivityForResult() 跳轉至 ActivityB 中,並在 ActivityB 中的 FragmentB 裏面返回到 ActivityA,返回代碼如下:

1
2
3
4
Intent intent = new Intent();
// putExtra
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().finish();

在回調時,先會回調 ActivityA 中的 onActivityResult() 方法,然後再分發回調 FragmentA 中的 onActivityResult() 方法,從 FragmentActivity 類的源碼中可以看出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Dispatch incoming result to the correct fragment.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    mFragments.noteStateNotSaved();
    int requestIndex = requestCode>>16;
    if (requestIndex != 0) {
        requestIndex--;

        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return;
    }

    super.onActivityResult(requestCode, resultCode, data);
}

再拓展一下,如果 FragmentA 中又嵌入一層 FragmentAA ,然後從 FragmentAA 中跳轉至 ActivityB,那麼在 FragmentAA 中的 onActivityResult() 方法中能收到回調嗎?顯然不能。從上述源碼中可以看出 FragmentActivity 只進行到一級分發。所以,如果想實現多級分發,就得自己在各級 Fragment 中手動添加分發代碼,至下一級 Fragment 中。

狀態變遷監聽


Fragment 的 hide 和 show 等狀態變遷操作都會反應在相應的回調函數中,我們可以利用這些監聽函數做一些界面刷新等功能。較爲常見的一個監聽函數就是 onHiddenChanged() 方法,這個方法的變化直接影響着 isHidden() 方法的返回值。

除了 isHidden() 方法,還有一個 isVisible() 方法,也用於判斷 Fragment 的狀態,表明 Fragment 是否對用戶可見,如果爲 true,必須滿足三點條件:1,Fragment 已經被 add 至 Activity 中;2,視圖內容已經被關聯到 window 上;3. 沒有被隱藏,即 isHidden() 爲 false。這三點,從 isVisible() 源碼中可以看出:

1
2
3
4
5
6
7
8
9
/**
* Return true if the fragment is currently visible to the user.  This means
* it: (1) has been added, (2) has its view attached to the window, and 
* (3) is not hidden.
*/
final public boolean isVisible() {
    return isAdded() && !isHidden() && mView != null
        && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE;
}

注意:onHiddenChanged() 方法可以監聽 hide() 和 show() 操作,與 setUserVisibleHint() 方法有所不同,後者常見的出現場景是在 ViewPager 和 Fragment 組合的 FragmentPagerAdapter 中使用。ViewPager 滑動時便是通過這個方法改變 Fragment 的狀態,利用這個方法可以實現 Fragment 懶加載,後續文章中再詳細描述實現方式。

參考鏈接


從上面這些介紹中可以看出,Fragment 雖然使用起來很方便,但卻存在很多問題,用久了你就會發現,有踩不完的坑等着你。當然,每個坑都有對應的解決方案,Google 一下,遍地開花,總能找到你所需要的內容。譬如這些系列文章,滿是乾貨:

發佈了44 篇原創文章 · 獲贊 87 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章