Activity和fragment是如何互動的

Aibot發表於2024-06-18
Fragment 是 Android 中歷史十分悠久的一個元件,在 Android 3.0 (API 級別 11)的時候推出,時至今日已成為 Android 開發中最常用的元件之一

在一開始的時候,引入 Fragment 的目的是為了在大螢幕(如平板電腦)上能夠更加動態和靈活地設計介面,被定義為一個 **輕量級 Activity** 而進行設計。透過 Fragment 可以將整個介面劃分為多個分散的區域塊,且同個 Fragment 可以被應用於多個 Activity 中,從而實現介面的**模組化**並提高**可重用性**。隨著 Android 系統的逐漸升級,系統功能越來越豐富,Fragment 因此也自然而然的就被新增了很多和 Activity 完全一樣的 API。例如,想要跳轉到某個 Activity 並獲取返回值,Activity 和 Fragment 就都加上了`startActivityForResult`方法;在 6.0 的時候有了執行時許可權,就都加上了`requestPermissions`方法;在 8.0 的時候有了畫中畫模式,就又都加上了`onPictureInPictureModeChanged`方法

隨著系統更迭,Fragment 逐漸變得不再**輕量**,繁雜的功能讓其越來越複雜,也導致以前的版本中暗坑無數,framework 層中的`android.app.Fragment`和 support 包中的 `android.support.v4.app.Fragment` 如今都被廢棄不再維護了,也遺留了很多個沒有解決的 bug,因此 Fragment 在長久以來並不能說是一個多麼讓開發者喜歡的元件

而到了如今 AndroidX & Jetpack 的年代,Google 官方也終於開始重新構思 Fragment 的定位,並對 Fragment 進行了大量重構。引用官方的說法:**我們希望 Fragment 成為一個真正的核心元件,它應該擁有可預測的、合理的行為,不應該出現隨機錯誤,也不應該破壞現有的功能。我們希望挑個時間釋出 Fragment 的 2.0 版,它將只包含那些新的、好用的 API。但在時機成熟之前,我們會在現有的 Fragment 中逐步加入新的並棄用舊的 API,併為舊功能提供更好的替代方案。當沒人再使用已棄用的 API 時,遷移到 Fragment 2.0 就會變得很容易**

本篇文章就來介紹新時代 AndroidX Fragment 的方方方面,陸陸續續寫了一萬多字,有基礎知識也有新知識,也許就包含了一些你還沒了解過的知識點,看完之後你會發現 Fragment 如今好像真的在變得越來越好用了,希望對你有所幫助 🤣🤣

本文所有示例程式碼基於以下版本進行講解

```groovy
dependencies {
    implementation "androidx.appcompat:appcompat:1.3.1"
    implementation "androidx.fragment:fragment:1.3.6"
    implementation "androidx.fragment:fragment-ktx:1.3.6"
}
```

# 1、如何使用

本節內容先來介紹如何將 Fragment 新增到 Activity 中

## 1、宣告 Fragment

目前 Fragment 已支援直接在建構函式中傳入 layoutId,並在 `onCreateView` 方法中自動完成 View 的 `inflate` 操作,這樣子類就無需重寫 `onCreateView` 方法了

```java
public class Fragment implements ComponentCallbacks, View.OnCreateContextMenuListener, LifecycleOwner,
        ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner,
        ActivityResultCaller {

    @LayoutRes
    private int mContentLayoutId;

    @ContentView
    public Fragment(@LayoutRes int contentLayoutId) {
        this();
        mContentLayoutId = contentLayoutId;
    }

    @MainThread
    @Nullable
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if (mContentLayoutId != 0) {
            return inflater.inflate(mContentLayoutId, container, false);
        }
        return null;
    }

}
```

因此,最簡單的情況下我們僅需要一行程式碼就可以宣告一個 Fragment 子類

```kotlin
class PlaceholderFragment : Fragment(R.layout.fragment_placeholder)
```

## 2、新增 Fragment

Fragment 一般情況下都需要和 FragmentActivity 組合使用,而我們日常使用的 AppCompatActivity 就已經直接繼承於 FragmentActivity 了。此外,雖然 Fragment 可以選擇任意 ViewGroup 作為其容器,但官方強烈推薦使用 FrameLayout 的子類 FragmentContainerView,因為其修復了 Fragment 在執行轉場動畫時的一些問題

我們可以為 FragmentContainerView 宣告 name 屬性,指定 Fragment 的全名路徑,這樣 Activity 在載入佈局檔案的時候就會自動完成 Fragment 的例項化和 add 操作了

```xml
<?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">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="github.leavesc.fragment.PlaceholderFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
```

如果想要透過程式碼在合適的時機再來主動注入 Fragment,那麼也可以不宣告 name 屬性,改為透過 `supportFragmentManager` 來主動執行 `add` 操作。此外,由於當發生 Configuration Change 時,系統會自動恢復重建每個 Activity 和 Fragment,因此我們需要主動判斷當前 Activity 是否屬於正常啟動,對應`savedInstanceState == null`,此時才去主動新增 Fragment,否則就會造成兩個 Fragment 重疊在一起

```kotlin
/**
 * @Author: leavesCZY
 * @Desc:
 * @公眾號:位元組陣列
 */
class MyFragmentActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my_fragment)
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                add(R.id.fragmentContainerView, PlaceholderFragment())
                setReorderingAllowed(true)
                addToBackStack(null)
            }
        }
    }

}
```

`supportFragmentManager.commit`方法是`fragment-ktx`庫中提供的擴充套件方法,實際上也只是對 FragmentManager 和 FragmentTransaction 做了一層封裝

```kotlin
public inline fun FragmentManager.commit(
    allowStateLoss: Boolean = false,
    body: FragmentTransaction.() -> Unit
) {
    val transaction = beginTransaction()
    transaction.body()
    if (allowStateLoss) {
        transaction.commitAllowingStateLoss()
    } else {
        transaction.commit()
    }
}
```

> 本文大部分的示例程式碼都不會去考慮 Activity 銷燬重建的情況,但讀者在實際開發中需要考慮這種情況

# 2、生命週期

## 1、初始

不管 Fragment 的直接載體是什麼,最終都必須託管在 Activity 中,Fragment 包含有很多個生命週期回撥方法,其生命週期會直接受宿主 Activity 生命週期的影響,但也還來源於其它方面。例如,Fragment 的直接載體可以是 Activity 或者是其它更上層的 Fragment,Fragment 會自動隨著載體生命週期的變化而變化;如果直接載體是 ViewPager2 的話,切換 ViewPager2 的時候 Fragment 就會單獨發生變化

關於 Fragment 的生命週期有一張比較經典的圖片,直接標明瞭 Activity 和 Fragment 在各個生命週期狀態的對映關係

![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e238f31725e94e05a4690b3a5c3b9db4~tplv-k3u1fbpfcp-watermark.image)

Fragment 的大部分生命週期方法都和 Activity 相對映,但兩者的生命週期方法有著明確的先後順序。以一個透過 FragmentContainerView 新增到 Activity 中的 Fragment 為例,從啟動 Activity 到按返回鍵退出頁面的整個過程中,生命週期的變化是:

- Activity 的 onCreate **方法裡** 呼叫 Fragment 的 onAttach(Context)、onAttach(Activity)、onCreate
- Activity 的 onStart **方法裡** 呼叫 Fragment 的 onCreateView、onViewCreated、onActivityCreated、onViewStateRestored、onStart
- Activity 的 onResume **方法後** 呼叫 Fragment 的 onResume
- Activity 的 onPause **方法裡** 呼叫 Fragment 的 onPause
- Activity 的 onStop **方法裡** 呼叫 Fragment 的 onStop
- Activity 的 onDestroy **方法裡** 呼叫 Fragment 的 onDestroyView、onDestroy、onDetach

可以看到,整個生命週期以 `onResume` 方法作為分割線,該方法被回撥意味著檢視已經處於前臺活躍狀態了,Activity 作為 Fragment 的載體,就需要先保證其自身的 `onResume` 方法已經回撥結束了才能去回撥 Fragment 的 `onResume` 方法,因此兩者不存在巢狀呼叫關係。而對於其它方法,當被回撥時就意味著 Activity 處於非活躍狀態或者是即將被銷燬,此時就需要先回撥完成 Fragment 的方法再結束自身,因此就存在巢狀呼叫關係

---

此外,如果 Activity 啟動了另外一個 Activity,此時其 `onSaveInstanceState` 方法就會被呼叫,此方法一樣會巢狀呼叫 Fragment 的相應方法,但該方法和 `onStop` 方法的先後順序在不同系統版本上有著差異性。在 API 28 之前的所有版本,`onSaveInstanceState()`會在 `onStop()`方法之前被呼叫,API 28+ 則相反

![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a639b442db6c46f4bd57ec0ead3cf157~tplv-k3u1fbpfcp-watermark.image)

當 Activity 從後臺返回前臺時,如果該 Activity 屬於銷燬重建的情況的話,Fragment 會重新走一遍生命週期方法,此時其 `onCreate、onCreateView、onActivityCreated、onViewStateRestored` 方法的引數值 Bundle 就不為 null,當中就保留了我們在 `onSaveInstanceState`方法中插入的值,我們可以依靠該 Bundle 來幫助頁面重建

---

此外,目前 `onAttach(Activity)` 和 `onActivityCreated(Bundle)` 這兩個方法已經被標記為廢棄了,這兩個方法的初衷是為了讓 Fragment 的邏輯能夠和載體 Activity 之間建立聯絡,並得到來自於 Activity 的 `onCreate` 事件通知,這就導致了開發者有可能會寫出和 Activity 強關聯的邏輯,官方現在不鼓勵這種耦合。View 相關的邏輯應該放在 `onViewCreated` 方法中處理,其它初始化程式碼應該在 `onCreate` 方法中處理,Fragment 的初始化邏輯不應該去依賴於這兩個廢棄的方法,Fragment 應該更加獨立才對

如果實在需要得到 Activity 的 `onCreate` 事件通知,可以透過在 `onAttach(Context)`方法中新增 LifecycleObserver 來實現

```kotlin
override fun onAttach(context: Context) {
    super.onAttach(context)
    requireActivity().lifecycle.addObserver(object : DefaultLifecycleObserver {
        override fun onCreate(owner: LifecycleOwner) {
            owner.lifecycle.removeObserver(this)
            //TODO            
        }
    })
}
```

## 2、回退棧 & 事務

Fragment 的生命週期不僅僅只有以上那種線性流程那麼簡單,不然 Fragment 也不會總是被人吐槽狀態過於複雜多變了。Fragment 生命週期的開始節點是 `onAttach` 方法,結束節點是 `onDetach` 方法,這個過程中 `onCreateView` 到 `onDestroyView` 之間可以被執行 N 多次,就像下圖所示

![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b7275ec937248a28b5e522513e70e98~tplv-k3u1fbpfcp-watermark.image)

 `onCreateView` 到 `onDestroyView` 方法之間之所以可能會執行 N 多次,就在於 FragmentTransaction 可以先後執行 N 多次 **移除(remove)**或者 **替換(replace)**操作,即先後載入不同 Fragment。新載入的 Fragment 就會取代之前的 Fragment 切換到前臺,舊的 Fragment 的檢視 View 就會被銷燬。如果載入新 Fragment 的操作有新增到**回退棧**中,那麼當使用者點選返回鍵時舊的 Fragment 就會重新呈現到前臺,此時就會重新走一遍 `onCreateView` 到 `onDestroyView` 方法之間的流程了

這裡就涉及到了關於 **Fragment 回退棧** 的概念了。假設 FragmentA 透過以下程式碼 replace 為了 FragmentB,那麼當使用者按返回鍵時,FragmentB 就會被銷燬,而 FragmentA 就會重新執行 `onCreateView` 到 `onDestroyView` 之間的所有方法,重新回到前臺

```kotlin
supportFragmentManager.commit {
    setReorderingAllowed(true)
    addToBackStack(null)
    replace(
        R.id.fragmentContainerView, FragmentB()
    )
}
```

**我們可以透過點選返回鍵退出 FragmentB,因此直觀感受上似乎就是 FragmentB 被新增到了回退棧中,但實際上被新增的並不是 Fragment,而是那些包含了 `addToBackStack(String)` 方法的一整個事務**

這句話看著很抽象,我舉一個例子來幫助讀者理解

我們先 add 一個 Fragment 1,然後再 replace 為 Fragment 2,replace 事務中就呼叫了 `addToBackStack(null)` 方法,那麼當點選返回鍵時,Fragment 2 就會被銷燬,Fragment 1 重新載入到前臺頁面。大致程式碼以及執行效果:

```kotlin
supportFragmentManager.commit {
    setReorderingAllowed(true)
    addToBackStack(null)
    add(R.id.fragmentContainerView, Fragment1())
}

supportFragmentManager.commit {
    setReorderingAllowed(true)
    addToBackStack(null)
    replace(R.id.fragmentContainerView, Fragment2())
}
```

![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d24f4ff05bc4755a8ec32b8a6e7af84~tplv-k3u1fbpfcp-watermark.image)

從執行 add 操作到點選返回鍵,兩個 Fragment 的生命週期方法的呼叫順序就大致如下所示,省去了部分方法

```kotlin
//先執行 add 操作
Fragment-1: onStart-start
Fragment-1: onResume-start

//再執行 replace 操作
Fragment-1: onPause-start
Fragment-1: onStop-start

Fragment-2: onAttach-context-start
Fragment-2: onCreate-start
Fragment-2: onResume-start

Fragment-1: onDestroyView-start

//點選返回鍵
Fragment-2: onPause-start
Fragment-2: onStop-start

Fragment-1: onCreateView-start
Fragment-1: onActivityCreated-start
Fragment-1: onViewStateRestored-start
Fragment-1: onStart-start
Fragment-1: onResume-start

Fragment-2: onDestroyView-start
Fragment-2: onDestroy-start
Fragment-2: onDetach-start
```

這整個過程的呼叫關係可以總結為:

- 當執行 replace 操作時。Fragment 1 會先執行到 `onStop`,意味著 Fragment 1 已經被切換到後臺了。然後就開始執行 Fragment 2 的生命週期直到 `onResume` ,意味著 Fragment 2 已經被切換到前臺了。之後就會接著執行 Fragment 1 的 `onDestroyView` ,此時就意味著 Fragment 1 不僅被切換到了後臺,其 View 也被銷燬了
- 當點選返回鍵時。Fragment 2 會先執行到 `onStop`,意味著 Fragment 2 已經被切換到後臺了。Fragment 1 則會再次執行從 `onCreateView` 到 `onResume` 之間的所有方法,意味著重新回到了前臺。之後就會接著執行 Fragment 2 的生命週期直到 `onDetach` ,此時 Fragment 2 就被完全銷燬了,無法再次回到該例項頁面

點選返回鍵時,由於 replace 操作呼叫了`addToBackStack(null)` 方法,意味著該事務加入到了回退棧中,因此此時響應了返回鍵事件的其實就是該事務,所以 replace 操作就會被撤銷,FragmentManager 負責將檢視恢復到 replace 之前的狀態,因此 Fragment 2 整個例項被完全銷燬了,Fragment 1 得以重新回到前臺

所以說,**FragmentTransaction 的回退棧中保留的是事務而非具體的 Fragment 例項,能響應返回事件的是我們向其中提交的事務,具體的響應結果就是將該事務撤銷,恢復到之前的狀態**

---

如果以上例子覺得還不夠明白,可以再舉一個例子 ~~

我們先 add 一個 Fragment 1,然後再 replace 為 Fragment 2,replace 事務中**不呼叫** `addToBackStack(null)` 方法,那麼此時就需要點選兩次返回鍵才能退出 Fragment 2 了,且此時 Activity 也會隨著退出。大致程式碼以及執行效果:

```kotlin
supportFragmentManager.commit {
    setReorderingAllowed(true)
    addToBackStack(null)
    add(R.id.fragmentContainerView, Fragment1())
}

supportFragmentManager.commit {
    setReorderingAllowed(true)
    //addToBackStack(null)
    replace(R.id.fragmentContainerView, Fragment2())
}
```

![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a65f87c69fa448d3ae051606bd15d2cf~tplv-k3u1fbpfcp-watermark.image)

從執行 add 操作到點選兩次返回鍵,兩個 Fragment 的生命週期方法的呼叫順序就大致如下所示,省去了部分方法

```kotlin
//先執行 add 操作
Fragment-1: onStart-start
Fragment-1: onResume-start

//再執行 replace 操作
Fragment-1: onPause-start
Fragment-1: onStop-start

Fragment-2: onAttach-context-start
Fragment-2: onCreate-start
Fragment-2: onResume-start

Fragment-1: onDestroyView-start

//點選返回鍵
Fragment-1: onDestroy-start
Fragment-1: onDetach-start

//再次點選返回鍵
Fragment-2: onPause-start
Fragment-2: onStop-start
Fragment-2: onDestroyView-start
Fragment-2: onDestroy-start
Fragment-2: onDetach-start
```

- 當第一次點選返回鍵時。由於 replace 事務並沒有被新增到回退棧中,而 add 操作有,所以此時響應了返回事件的是 add 操作,點選返回鍵就相當於把 add 操作給撤銷掉了,因此 Fragment 1 就會執行到 `onDetach` 方法,Fragment 2 不受影響
- 當第二次點選返回鍵時,由於此時 FragmentTransaction 的回退棧為空,所以此時響應了返回事件的其實是 Activity,所以 Activity 會退出,連帶著 Fragment 2 也一起被銷燬了

看完這兩個例子,讀者應該明白了吧?在回退棧中,呼叫了`addToBackStack(String)` 方法的事務才是重點,Fragment 並不是

## 3、FragmentLifecycle

在最早的時候,**生命週期**這個詞對於 Activity 和 Fragment 來說指的就是其特定的回撥方法是否已經被執行了,例如 `onCreate`、`onStart`、`onDestroy` 等方法。現如今也指這兩者的 `Lifecycle.State` 的當前值是什麼

Activity 和 Fragment 都實現了 LifecycleOwner 介面,都包含了一個 Lifecycle 物件用於標記其生命週期狀態,因此我們能夠在這兩者中以和生命週期繫結的方式對 LiveData 進行監聽,就像以下程式碼一樣,當中 textLiveData 關聯的 this 即 LifecycleOwner 物件,從而保證了只有當 Fragment 處於前臺活躍狀態時才會收到資料回撥

```kotlin
class PageFragment : Fragment() {

    private val pageViewModel by lazy {
        ViewModelProvider(this@PageFragment).get(PageViewModel::class.java).apply {
            textLiveData.observe(this@PageFragment, {

            })
        }
    }
    
}
```

`Lifecycle.State` 一共包含五種值,FragmentLifecycle 會在這五個值中不斷流轉,例如當切換為 DESTROYED 狀態時,也即意味 `onDestory()、onDetach()` 等方法被呼叫了,至此 Fragment 的本次生命週期也就結束了

```kotlin
public enum State {
    DESTROYED,
    INITIALIZED,
    CREATED,
    STARTED,
    RESUMED;
}
```

## 4、FragmentViewLifecycle

Fragment 相對於 Activity 來說比較特殊,因為其關聯的 View 物件可以在單次 FragmentLifecycle 過程中先後多次載入和銷燬,因此實際上 **FragmentView 的生命週期和 Fragment 並不同步**

Fragment 內部也宣告瞭九種狀態值用於標記其自身的生命週期狀態,當中就包含一個 VIEW_CREATED,即表示 FragmentView 已經被建立了,從這也可以看出 Fragment 內部維護的生命週期密度要比 FragmentLifecycle 小得多

```java
static final int INITIALIZING = -1;          // Not yet attached.
static final int ATTACHED = 0;               // Attached to the host.
static final int CREATED = 1;                // Created.
static final int VIEW_CREATED = 2;           // View Created.
static final int AWAITING_EXIT_EFFECTS = 3;  // Downward state, awaiting exit effects
static final int ACTIVITY_CREATED = 4;       // Fully created, not started.
static final int STARTED = 5;                // Created and started, not resumed.
static final int AWAITING_ENTER_EFFECTS = 6; // Upward state, awaiting enter effects
static final int RESUMED = 7;                // Created started and resumed.
```

狀態值切換到 DESTROYED,對於 FragmentLifecycle 來說意味著 `onDestroy、onDetach` 等方法被呼叫,對於 FragmentViewLifecycle 來說則意味著`onDestroyView`方法被呼叫,因此 FragmentViewLifecycle 的跨度範圍要比 FragmentLifecycle 小一些。而且 FragmentLifecycle 切換到 DESTROYED 後狀態值是不可逆的,無法再次更改,而 FragmentViewLifecycle 切換到 DESTROYED 後是有機會再次更改的,因為 View 物件可以先後多次載入和銷燬,每次載入就意味著生命週期的重新開始

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac4e51b89052484ea83c8f6b14d05f82~tplv-k3u1fbpfcp-watermark.image)

Fragment 提供了一個`getViewLifecycleOwner()`方法由於提供 FragmentViewLifecycle,從中可以看出該方法只能在 `onCreateView()` 到 `onDestroyView()` 之間被呼叫,即只能在 FragmentView 建立了且銷燬之前使用,否則將直接丟擲異常

```java
@Nullable
FragmentViewLifecycleOwner mViewLifecycleOwner;

@MainThread
@NonNull
public LifecycleOwner getViewLifecycleOwner() {
    if (mViewLifecycleOwner == null) {
        throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
                + "getView() is null i.e., before onCreateView() or after onDestroyView()");
    }
    return mViewLifecycleOwner;
}

```

---

FragmentViewLifecycle 非常有用,我們在日常開發中可以根據實際情況使用 FragmentViewLifecycle 來替代 FragmentLifecycle,因為 FragmentLifecycle 存在著一些並不明顯的使用誤區,很容易就造成 bug

舉個例子。假設我們的 Fragment 需要監聽 ViewModel 中某個 LiveData 值的變化,並根據監聽到的值來設定介面,此時就要考慮在 Fragment 中的哪裡來訂閱 LiveData 了。看了以上內容後,我們已經知道 `onCreateView` 方法到 `onDestoryView`之間是可能會先後執行多次的,那麼監聽操作就不應該放在這裡面了,否則就會造成重複訂閱。此時我們想到的可能就是這兩種方式了:**宣告全域性變數 ViewModel 時順便監聽** 或者是 **在 onCreate 方法中進行監聽**

```kotlin
private val pageViewModel by lazy {
    ViewModelProvider(this).get(PageViewModel::class.java).apply {
        textLiveData.observe(this@PageFragment, Observer {
            //TODO
        })
    }
}
```

```kotlin
private val pageViewModel by lazy {
    ViewModelProvider(this).get(PageViewModel::class.java)
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    pageViewModel.textLiveData.observe(this@PageFragment, Observer {
        //TODO
    })
}
```

這兩種方式都可以避免重複訂閱的問題,但此時還存在另一個藏得很深的問題:**假如 FragmentView 真的銷燬重建了,重建後的 FragmentView 也收不到 textLiveData 已有的資料!!!**

會出現該問題的本質原因還是因為 FragmentView 的生命週期和 Fragment 並不同步。如果 Fragment 已經接收過 textLiveData 的回撥了,那麼當 FragmentView 銷燬重建後,由於 textLiveData 的值沒有發生變化,和 textLiveData 繫結的 LifecycleOwner 也還一直存在著,那麼重建後的 FragmentView 自然就不會收到 textLiveData 的回撥了,從而導致無法根據回撥來重建頁面

為了解決該問題,就需要使用到 FragmentViewLifecycle 了。由於 FragmentViewLifecycle 的生命週期在 `onDestoryView`的時候就結束了,此時也會自動移除 Observer,因此我們可以直接在 `onViewCreated` 方法中使用 `viewLifecycleOwner` 來監聽 textLiveData,從而保證每次重建後的 FragmentView 都能收到回撥

```kotlin
private val pageViewModel by lazy {
    ViewModelProvider(this).get(PageViewModel::class.java)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    pageViewModel.textLiveData.observe(viewLifecycleOwner, Observer {
        //TODO
    })
}
```

在大部分情況下,我們在 Fragment 中執行的操作都是和 FragmentView 強關聯的,屬於檢視操縱行為,此時就可以使用 FragmentViewLifecycle 來替代 FragmentLifecycle,從而保證事件一定只有在 FragmentView 存在且活躍的情況下才會被回撥,且保證了每次 FragmentView 被銷燬重建的時候都能夠得到最新資料。而對於那些依賴於 Fragment 完整生命週期的事件,就還是隻能繼續使用 FragmentLifecycle 了

> 關於 Lifecycle 和 LiveData 的原始碼解析可以看這兩篇文章:
>
> - [從原始碼看 Jetpack(1)-Lifecycle 原始碼詳解](https://juejin.cn/post/6847902220755992589)
> - [從原始碼看 Jetpack(3)-LiveData 原始碼詳解](https://juejin.cn/post/6847902222345633806)

## 5、未來的計劃

從以上內容讀者應該可以看出來,Fragment 的生命週期是十分複雜的,是否加入回退棧對於生命週期的影響很大,而且單個 Fragment 例項的生命週期還分為了 FragmentLifecycle 和 FragmentViewLifecycle 兩種,這也是 Fragment 一直被人吐槽的最大原因之一。幸運的是,Google 官方現在也意識到了這個問題,並且有將兩者進行合併簡化的計劃

以下內容引用至 Google 官方:

最後要說的問題,是 Fragment 的生命週期。當前 Fragment 的生命週期十分複雜,它包含了兩套不同的生命週期。Fragment 自己的生命週期從它被新增到 FragmentManager 的時候開始,一直持續到它被 FragmentManager 移除並銷燬為止;而 Fragment 所包含的檢視,則有一個完全分離的生命週期。當您的 Fragment 進入回退棧時,檢視將會被銷燬。但 Fragment 則會繼續存活

於是我們產生了一個大膽的想法: 將兩者合二為一會怎麼樣?在 Fragment 檢視銷燬時便銷燬 Fragment,想要重建檢視時就直接重建 Fragment,這樣的話將大大減少 Fragment 的複雜度。而諸如 FragmentFactory 和狀態儲存一類,以往在 onConfigrationChange、 程序的死亡和恢復時使用的方法,在這種情況下將會成為預設選項

當然,這個改動將會是十分的巨大。我們目前處理的方式,是將它作為一個可選 API 加入到了 FragmentActivity 中。使用了這個新的 API,就可以開啟生命週期簡化過的新世界

# 3、setMaxLifecycle

在以前,Fragment 往往會和 ViewPager 配套使用,由於 ViewPager 存在頁面**預載入**的機制,相鄰 Fragment 會在對使用者不可見的時候就執行`onResume`方法,從而觸發一些不必要的業務邏輯,導致多餘的流量消耗和效能消耗

為了解決以上問題,實現 Fragment **懶載入**的效果,我們需要在`onViewCreated`、`onResume`、`onHiddenChanged`、`setUserVisibleHint`等多個方法中新增標記位,透過組合標記位來準確得知 Fragment 當前是否真的對使用者可見,且只在可見的情況下才去觸發關聯的業務邏輯。這種實現方式長久以來一直很流行,也完全可用,但也真的不夠優雅,我們需要組合多個條件才能實現目的,且 Fragment 在不可見的時候就去回撥`onResume`方法本身也是一種很違反常識的行為

現如今我們也有了更好的選擇,`setUserVisibleHint`方法已經被廢棄了,從註釋可以看到官方現在推薦使用`setMaxLifecycle` 方法來更為精準地控制 Fragment 的生命週期

```kotlin
/**
 * @deprecated If you are manually calling this method, use
 * {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} instead. If
 * overriding this method, behavior implemented when passing in <code>true</code> should be
 * moved to {@link Fragment#onResume()}, and behavior implemented when passing in
 * <code>false</code> should be moved to {@link Fragment#onPause()}.
 */
@Deprecated
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded() && mIsCreated) {
        mFragmentManager.performPendingDeferredStart(
                mFragmentManager.createOrGetFragmentStateManager(this));
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
    if (mSavedFragmentState != null) {
        // Ensure that if the user visible hint is set before the Fragment has
        // restored its state that we don't lose the new value
        mSavedUserVisibleHint = isVisibleToUser;
    }
}
```

`setMaxLifecycle`方法從名字就可以看出來是用於為 Fragment 設定一個最大的生命週期狀態,實際上也的確是,state 引數值我們只能選擇 CREATED、STARTED、RESUMED 三者之一

```kotlin
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
        @NonNull Lifecycle.State state) {
    addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
    return this;
}
```

在正常情況下,我們在 Activity 中 add 一個 Fragment 後其生命週期流程是會直接執行到 `onResume` 方法的

```kotlin
supportFragmentManager.commit {
    val fragment = FragmentLifecycleFragment.newInstance(
        fragmentTag = (tagIndex++).toString(),
        bgColor = Color.parseColor("#0091EA")
    )
    add(
        R.id.fragmentContainerView, fragment
    )
    setReorderingAllowed(true)
    addToBackStack(null)
}
```

```kotlin
Fragment-1: onAttach
Fragment-1: onCreate
Fragment-1: onCreateView
Fragment-1: onViewCreated
Fragment-1: onActivityCreated
Fragment-1: onViewStateRestored
Fragment-1: onStart
Fragment-1: onResume
```

而如果設定了`setMaxLifecycle(fragment, Lifecycle.State.CREATED)`的話,就會發現 Fragment 只會執行到 `onCreate` 方法,且 Fragment 也不會顯示,畢竟 FragmentView 沒有被建立,自然也不會被掛載到 Activity 中

```kotlin
supportFragmentManager.commit {
    val fragment = FragmentLifecycleFragment.newInstance(
        fragmentTag = (tagIndex++).toString(),
        bgColor = Color.parseColor("#0091EA")
    )
    add(
        R.id.fragmentContainerView, fragment
    )
    setMaxLifecycle(fragment, Lifecycle.State.CREATED)
    setReorderingAllowed(true)
    addToBackStack(null)
}
```

```kotlin
Fragment-1: onAttach
Fragment-1: onCreate
```

而如果 Fragment 已經回撥了 `onResume` 方法,此時再來為其設定 `setMaxLifecycle(fragment, Lifecycle.State.STARTED)` 的話,就會發現 Fragment 會回撥 `onPause` 方法

```kotlin
Fragment-1: onAttach
Fragment-1: onCreate
Fragment-1: onCreateView
Fragment-1: onViewCreated
Fragment-1: onActivityCreated
Fragment-1: onViewStateRestored
Fragment-1: onStart
Fragment-1: onResume

Fragment-1: onPause
```

怎麼理解以上三種情況的差異呢?

其實,`setMaxLifecycle` 控制的是 FragmentLifecycle,同個 FragmentLifecycle 狀態值對應的可能是不同的生命週期方法,就像以下圖片所示。`onResume`方法對應的是 RESUMED,此時我們要求 FragmentLifecycle 的最大值是 STARTED,那麼 Fragment 只能執行`onPause` 方法將狀態值切換到 STARTED。而如果我們一開始就設定了最大值是 STARTED 的話,Fragment 就只會從 `onAttach` 執行到 `onStart` 方法,此時 FragmentLifecycle 一樣是 STARTED

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac4e51b89052484ea83c8f6b14d05f82~tplv-k3u1fbpfcp-watermark.image)

所以說,`setMaxLifecycle` 方法會根據 Fragment 的現有狀態來加大或者回退當前的 FragmentLifecycle,選擇合適的回撥路線來進行狀態切換

`setMaxLifecycle` 方法目前已經應用於 ViewPager2 中的 FragmentStateAdapter 了。當 Fragment 被切出時,就將其最大狀態設定為 STARTED,當 Fragment 被切入時,就將其最大狀態設定為 RESUMED,從而使得只有當前可見的 Fragment 才會被回撥 `onResume` 方法,被切出的 Fragment 則會回撥 `onPause` 方法,保證了每個 Fragment 都能處於正確的生命週期狀態

```java
void updateFragmentMaxLifecycle(boolean dataSetChanged) {
    ···
    Fragment toResume = null;
    for (int ix = 0; ix < mFragments.size(); ix++) {
        long itemId = mFragments.keyAt(ix);
        Fragment fragment = mFragments.valueAt(ix);

        if (!fragment.isAdded()) {
            continue;
        }

        if (itemId != mPrimaryItemId) {
            //重點
            transaction.setMaxLifecycle(fragment, STARTED);
        } else {
            toResume = fragment; // itemId map key, so only one can match the predicate
        }
        fragment.setMenuVisibility(itemId == mPrimaryItemId);
    }
    if (toResume != null) { // in case the Fragment wasn't added yet
        //重點
        transaction.setMaxLifecycle(toResume, RESUMED);
    }

    if (!transaction.isEmpty()) {
        transaction.commitNow();
    }
}
```

# 4、FragmentFactory

在以前,Fragment 要求開發者必須為每個子類均宣告一個**無參建構函式**,因為當系統發生 Configuration Change 時,系統需要恢復重建每個 Fragment,如果 Fragment 包含一個或者多個**有參建構函式**的話,系統不知道應該呼叫哪個建構函式,而且也無法自動生成**構造引數**,因此只能選擇透過**反射無參建構函式**的方式來完成例項化,這就要求每個子類都必須擁有一個無參建構函式了

為了解決該問題,按以往的方式我們都是透過宣告一個**靜態工廠方法**來提供例項化的入口,並透過 Bundle 來傳遞請求引數。當系統在恢復重建 Fragment 時也會自動將重建前的 Bundle 注入到新的例項中,從而保證請求引數不會缺失

```kotlin
class FragmentFactoryFragment : BaseFragment(R.layout.fragment_fragment_factory) {

    companion object {

        private const val KEY_BG_COLOR = "keyBgColor"

        fun newInstance(bgColor: Int): FragmentFactoryFragment {
            return FragmentFactoryFragment().apply {
                arguments = Bundle().apply {
                    putInt(KEY_BG_COLOR, bgColor)
                }
            }
        }

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val bgColor = arguments?.getInt(KEY_BG_COLOR) ?: throw IllegalArgumentException()
        val flRoot = view.findViewById<View>(R.id.flRoot)
        flRoot.setBackgroundColor(bgColor)
    }

}
```

透過反射無參建構函式來例項化 Fragment 並注入 `arguments` 的過程,就對應 Fragment 中的 `instantiate` 方法

```java
public static Fragment instantiate(@NonNull Context context, @NonNull String fname,
        @Nullable Bundle args) {
    try {
        Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(
                context.getClassLoader(), fname);
        //反射無參建構函式
        Fragment f = clazz.getConstructor().newInstance();
        if (args != null) {
            args.setClassLoader(f.getClass().getClassLoader());
            //注入 Bundle
            f.setArguments(args);
        }
        return f;
    } catch (java.lang.InstantiationException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (IllegalAccessException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (NoSuchMethodException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": could not find Fragment constructor", e);
    } catch (InvocationTargetException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": calling Fragment constructor caused an exception", e);
    }
}
```

為了解決**無法自由定義有參建構函式的問題**,Fragment 如今也提供了 FragmentFactory 來參與例項化 Fragment 的過程

首先,假設 Fragment 的建構函式需要一個 int 型別的構造引數

```kotlin
class FragmentFactoryFragment(private val bgColor: Int) :
    BaseFragment(R.layout.fragment_fragment_factory) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val flRoot = view.findViewById<View>(R.id.flRoot)
        flRoot.setBackgroundColor(bgColor)
    }

}
```

繼承 FragmentFactory,在 `instantiate`方法中判斷當前要例項化的是哪一個 Fragment,在此根據 bgColor 例項化 Fragment 並返回

```kotlin
class MyFragmentFactory(private val bgColor: Int) : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val clazz = loadFragmentClass(classLoader, className)
        if (clazz == FragmentFactoryFragment::class.java) {
            return FragmentFactoryFragment(bgColor)
        }
        return super.instantiate(classLoader, className)
    }

}
```

之後我們在程式碼中僅需要向 `supportFragmentManager` 宣告需要注入的 Fragment Class 即可,無需顯式例項化,例項化過程交由 MyFragmentFactory 來完成

```kotlin
class FragmentFactoryActivity : BaseActivity() {

    override val bind by getBind<ActivityFragmentFactoryBinding>()

    override fun onCreate(savedInstanceState: Bundle?) {
        //需要在 super.onCreate 之前呼叫,因為 Activity 需要依靠 FragmentFactory 來完成 Fragment 的恢復重建
        supportFragmentManager.fragmentFactory = MyFragmentFactory(Color.parseColor("#00BCD4"))
        super.onCreate(savedInstanceState)
        supportFragmentManager.commit {
            add(R.id.fragmentContainerView, FragmentFactoryFragment::class.java, null)
            setReorderingAllowed(true)
            disallowAddToBackStack()
        }
    }

}
```

FragmentFactory 的好處有:

- 將本應該直接傳遞給 Fragment 的構造引數轉交給了 FragmentFactory,這樣系統在恢復重建時就能統一透過 `instantiate` 方法來重新例項化 Fragment,而無需關心 Fragment 的建構函式
- 只要 FragmentFactory 包含了所有 Fragment 均需要的構造引數,那麼同個 FragmentFactory 就可以用於例項化多種不同的 Fragment,從而解決了需要為每個 Fragment 均宣告靜態工廠方法的問題,Fragment 也省去了向 Bundle 賦值取值的操作,減少了開發者的工作量

FragmentFactory 也存在著侷限性:

- 由於需要考慮 Fragment 恢復重建的場景,因此我們在 `super.onCreate` 之前就需要先初始化 `supportFragmentManager.fragmentFactory`,這樣 Activity 在恢復重建的時候才能根據已有引數來重新例項化 Fragment,這就要求我們必須在一開始的時候就確定 FragmentFactory 的構造引數,也即 Fragment 的構造引數,而這在日常開發中並非總是能夠做到的,因為 Fragment 的構造引數可能是需要動態生成的

# 5、Activity Result API

Activity 和 Fragment 中有兩組歷史悠久的 API 如今都已經被廢棄了:

- `startActivityForResult()`、`onActivityResult()` 
- `requestPermissions()`、`onRequestPermissionsResult()` 

雖然這兩組 API 所執行的操作並不一樣,但都屬於**發起請求並等待回撥結果**的操作型別,官方建議使用新增的 `registerForActivityResult` 方法來實現這類需求,因為透過 Activity Result API 實現的程式碼更有複用性,減少了模板程式碼且易於測試

看個小例子。假設存在一個 ActivityResultApiFragment 和一個 ActivityResultApiTestActivity。Fragment 用於輸入使用者名稱,在拿到使用者名稱後需要將值傳遞給 Activity,由其負責查詢使用者所在地,查到後再將值回傳給 Fragment

按照需求,userName 和 location 都需要儲存在 Intent 中,以便在兩者之間實現資料交換,儲存 userName 和取出 location 的過程都由 ActivityResultContract 來實現,對應 GetLocation

- GetLocation 的兩個泛型宣告就分別對應著**請求引數的資料型別**以及**返回引數的資料型別**
- `createIntent`方法用於儲存請求引數 userName,並指定要啟動的是 ActivityResultApiTestActivity
- `parseResult` 方法會拿到 ActivityResultApiTestActivity 返回的 Intent 物件,如果判斷為成功狀態的話則直接取出 location 並返回

```kotlin
class GetLocation : ActivityResultContract<String, String>() {

    companion object {

        private const val KEY_REQ_LOCATION = "keyReqLocation"

        private const val KEY_RES_LOCATION = "keyResLocation"

        fun getUserName(intent: Intent): String {
            return intent.getStringExtra(KEY_REQ_LOCATION)
                ?: throw RuntimeException("userName must not be null")
        }

        fun putResultOk(location: String): Intent {
            return Intent().apply {
                putExtra(KEY_RES_LOCATION, location)
            }
        }

    }

    override fun createIntent(context: Context, input: String): Intent {
        val intent = Intent(context, ActivityResultApiTestActivity::class.java)
        intent.putExtra(KEY_REQ_LOCATION, input)
        return intent
    }

    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        if (resultCode == Activity.RESULT_OK) {
            return intent?.getStringExtra(KEY_RES_LOCATION)
        }
        return null
    }

}
```

ActivityResultApiTestActivity 按照我們慣常的方式獲取到請求引數,返回 RESULT_OK 狀態碼和結果值

```kotlin
class ActivityResultApiTestActivity : BaseActivity() {

    override val bind by getBind<ActivityActivityResultApiTestBinding>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bind.btnFinish.setOnClickListener {
            val userName = GetLocation.getUserName(intent)
            setResult(Activity.RESULT_OK, GetLocation.putResultOk("$userName-廣州"))
            finish()
        }
    }

}
```

ActivityResultApiFragment 透過 `registerForActivityResult` 方法來註冊 GetLocation,需要的時候再透過`getLocation.launch("業志陳")`來傳入使用者名稱並啟動 GetLocation,最終在 `onActivityResult` 方法中獲取到請求結果

```kotlin
class ActivityResultApiFragment :
    Fragment(R.layout.fragment_activity_result_api) {

    private val getLocation: ActivityResultLauncher<String> =
        registerForActivityResult(
            GetLocation(),
            object : ActivityResultCallback<String> {
                override fun onActivityResult(result: String) {
                    Toast.makeText(activity, result, Toast.LENGTH_SHORT).show()
                }
            })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val btnStartActivity = view.findViewById<Button>(R.id.btnStartActivity)
        btnStartActivity.setOnClickListener {
            getLocation.launch("業志陳")
        }
    }

}
```

![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3cc3d421480f430e8d4e1b6d9f3906b8~tplv-k3u1fbpfcp-watermark.image)

類似地,官方也提供了一個用於請求許可權的 ActivityResultContract 實現類:RequestMultiplePermissions,透過該 Contract 我們也能以很簡單的方式完成以前割裂且麻煩的申請操作

```kotlin
class ActivityResultApiFragment :
    Fragment(R.layout.fragment_activity_result_api) {

    private val requestPermissions: ActivityResultLauncher<Array<String>> =
        registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions(),
            object : ActivityResultCallback<Map<String, Boolean>> {
                override fun onActivityResult(result: Map<String, Boolean>) {
                    for (entry in result) {
                        Toast.makeText(activity, entry.key + " " + entry.value, Toast.LENGTH_SHORT)
                            .show()
                    }
                }
            })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val btnRequestPermissions = view.findViewById<Button>(R.id.btnRequestPermissions)
        btnRequestPermissions.setOnClickListener {
            requestPermissions.launch(
                arrayOf(
                    Manifest.permission.ACCESS_NETWORK_STATE,
                    Manifest.permission.ACCESS_FINE_LOCATION
                )
            )
        }
    }

}
```

使用 Activity Result API 的好處有:

- 完全隔絕了請求的發起者和處理者之間的引用關係,發起者直接面向於 ActivityResultContract,無需知道請求最終是交由誰處理,這樣開發者就可以隨時替換 ActivityResultContract 的具體實現邏輯了,避免耦合且易於測試
- 請求的發起邏輯和處理邏輯都能夠得到複用,減少了重複程式碼
- 統一了 `startActivityForResult` 和 `requestPermissions`的請求方式,並提供了更加簡潔方便的回撥方法,避免了以前申請邏輯和回撥邏輯是完全割裂開的情況

# 6、Fragment Result API

Activity Result API 適用於 Fragment 和非載體 Activity 之間進行資料通訊的場景,如果是**多個 Fragment 之間**或者是 **Fragment 和載體 Activity 之間**要進行資料通訊的話,我們可以選擇的實現方法有很多種:

- Fragment 透過 FragmentManager 拿到另一個 Fragment 的例項物件,透過直接呼叫對方的方法來實現多個 Fragment 之間的資料通訊
- Fragment 和 Activity 之間相互註冊回撥介面,以此來實現 Fragment 和載體 Activity 之間的資料通訊
- Fragment 以 Activity 作為中轉站將事件轉交給其它 Fragment,以此來實現多個 Fragment 之間的資料通訊
- Fragment 和 Activity 同時持有相同一個 Activity 級別的 ViewModel 例項,以此來實現多個 Fragment 之間、Fragment 和載體 Activity 之間的資料通訊

以上四種方法都可以實現需求,但也存在著一些問題:

- 前三種方法造成了 Fragment 之間、Fragment 和 Activity 之間存在強耦合,且由於並不清楚對方處於什麼生命週期狀態,一不小心就可能導致 NPE
- 最後一種方法對於生命週期安全有保障,但如果只是簡單的資料通訊場景的話,使用 ViewModel 就有點大材小用了,ViewModel 比較適用於資料量稍大的業務場景

如今 Fragment 也有了一種新的選擇:FragmentResult。使用 FragmentResult 進行資料通訊不需要持有任何 Fragment 或者 Activity 的引用,僅需要使用 FragmentManager 就可以實現

看個小例子。宣告一個 Activity 和兩個 Fragment,分別向對方正在監聽的 requestKey 下發資料,資料透過 Bundle 來進行傳遞,Activity 和 Fragment 只需要面向特定的 requestKey 來傳送資料或者監聽資料即可,無需關心資料的來源和去向

- setFragmentResult 方法表示的是向 requestKey 下發資料
- setFragmentResultListener 表示的是對 requestKey 進行監聽

```kotlin
const val requestKeyToActivity = "requestKeyToActivity"

private const val requestKeyFirst = "requestKeyFirst"

private const val requestKeySecond = "requestKeySecond"

class FragmentResultApiAFragment : Fragment(R.layout.fragment_fragment_result_api_a) {

    private val requestKeyFirst = "requestKeyFirst"

    private val requestKeySecond = "requestKeySecond"

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val btnSend = view.findViewById<Button>(R.id.btnSend)
        val btnSendMsgToActivity = view.findViewById<Button>(R.id.btnSendMsgToActivity)
        val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
        btnSend.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("fromFragmentResultApiAFragment", Random.nextInt().toString())
            //向對 requestKeyFirst 進行監聽的 Fragment 傳遞資料
            parentFragmentManager.setFragmentResult(requestKeyFirst, bundle)
        }
        btnSendMsgToActivity.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("fromFragmentResultApiAFragment", Random.nextInt().toString())
            //向對 requestKeyToActivity 進行監聽的 Activity 傳遞資料
            parentFragmentManager.setFragmentResult(requestKeyToActivity, bundle)
        }
        //對 requestKeySecond 進行監聽
        parentFragmentManager.setFragmentResultListener(
            requestKeySecond,
            this,
            { requestKey, result ->
                tvMessage.text = "requestKey: $requestKey \n result: $result"
            })
    }

}

class FragmentResultApiBFragment : Fragment(R.layout.fragment_fragment_result_api_b) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val btnSend = view.findViewById<Button>(R.id.btnSend)
        val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
        btnSend.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("fromFragmentResultApiBFragment", Random.nextInt().toString())
            //向對 requestKeySecond 進行監聽的 Fragment 傳遞資料
            parentFragmentManager.setFragmentResult(requestKeySecond, bundle)
        }
        //對 requestKeyFirst 進行監聽
        parentFragmentManager.setFragmentResultListener(
            requestKeyFirst,
            this,
            { requestKey, result ->
                tvMessage.text = "requestKey: $requestKey \n result: $result"
            })
    }

}

class FragmentResultApiActivity : BaseActivity() {

    override val bind by getBind<ActivityFragmentResultApiBinding>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager.setFragmentResultListener(
            requestKeyToActivity,
            this,
            { requestKey, result ->
                bind.tvMessage.text = "requestKey: $requestKey \n result: $result"
            })
    }

}
```

![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a7ec84eb47a4ff3a4cd3c98aff613ce~tplv-k3u1fbpfcp-watermark.image)

使用 Fragment Result API 的好處有:

- Fragment Result API 並不侷限於 Fragment 之間,只要 Activity 也持有和 Fragment 相同的 FragmentManager 例項,也可以用以上方式在 Activity 和 Fragment 之間實現資料通訊。但需要注意,一個 requestKey 所屬的資料只能被一個消費者得到,後註冊的 FragmentResultListener 會把先註冊的給覆蓋掉
- Fragment Result API 也可以用於在父 Fragment 和子 Fragment 之間傳遞資料,但前提是兩者持有的是相同的 FragmentManager 例項。例如,如果要將資料從子 Fragment 傳遞給父 Fragment,父 Fragment 應透過`getChildFragmentManager()` 來呼叫`setFragmentResultListener()`,而不是`getParentFragmentManager()`
- 資料的傳送者只負責下發資料,不關心也不知道有多少個接收者,也不知道資料是否有被消費。資料的接收者只負責接收資料,不關心也不知道有多少個資料來源。避免了 Activity 和 Fragment 之間存在引用關係,使得每個個體之間更加獨立
- 只有當 Activity 和 Fragment 至少處於 STARTED 狀態,即只有處於活躍狀態時才會接收到資料回撥,非活躍狀態下連續傳值也只會保留最新值,當切換到 DESTROYED 狀態時也會自動移除監聽,從而保證了生命週期的安全性

每一個載體(Activity 或者 Fragment)都包含一個和自身同等級的 FragmentManager 用於管理子 Fragment,對應 Activity 的 `supportFragmentManager` 和 Fragment 的 `childFragmentManager`;每一個 子 Fragment 也都包含一個來自於載體的 FragmentManager,對應 Fragment 的 `parentFragmentManager` 

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9cbcff81baeb4442adcf6ed448ac9ed5~tplv-k3u1fbpfcp-watermark.image)

# 7、OnBackPressed

Activity 包含有一個 `onBackPressed`方法用於響應使用者點選返回鍵的操作,可用來控制是否允許使用者退出當前頁面,但 Fragment 並不包含類似方法。如果想要讓 Fragment 也可以攔截使用者的返回操作的話,按照以往的方法就需要透過在 Activity 和 Fragment 之間建立回撥來進行攔截,這無疑也造成了兩者之間存在耦合

如今開發者有了更好的實現方式:透過 OnBackPressedDispatcher 使得任意可以訪問 Activity 的程式碼邏輯都可以攔截 `onBackPressed`方法

我們可以在 Fragment 中向 Activity 新增一個 OnBackPressedCallback 回撥,傳遞的值 true 即代表該 Fragment 會攔截使用者的每一次返回操作並進行回撥,我們需要根據業務邏輯在合適的時候將其置為 false,從而放開對`onBackPressed`的控制權。此外,`addCallback` 方法的第一個引數是 LifecycleOwner 型別,也即當前的 Fragment 物件,該引數確保了僅在 Fragment 的生命週期至少處於 `ON_START` 狀態時才進行回撥,並在 `ON_DESTROY` 時自動移除監聽,從而保證了生命週期的安全性

```kotlin
class PlaceholderFragment(private val sectionNumber: Int) :
    Fragment(R.layout.fragment_placeholder) {

    private val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            //TODO       
        }
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
    }
    
}
```

# 8、Fragment 妙用

## 1、監聽生命週期

我們在使用 Fragment 時往往都會為其指定佈局檔案,從而顯示特定檢視。但 Fragment 也可以不載入任何佈局檔案,此時 Fragment 就是完全無 UI 介面的,但只要該 Fragment 也掛載到了 Activity 或者父 Fragment 上,一樣可以收到相應的生命週期回撥,此時就可以透過**對上層完全無感知的形式**來間接獲得載體的生命週期回撥通知了

根據 Fragment 的這個特性實現的比較出名的元件和開源庫有:Jetpack Lifecycle 和 Glide

### Jetpack Lifecycle

熟悉 Jetpack 的同學應該都知道,我們是透過 Lifecycle 來收到 Activity 生命週期狀態變化的通知的,而 Lifecycle 就是透過向 Activity 注入一個無 UI 介面的 Fragment 得知 Activity 生命週期狀態變化的

現在我們在日常開發中使用的 AppCompatActivity 最終會繼承於 ComponentActivity,ComponentActivity 的 `onCreate` 方法是這樣的:

```kotlin
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ReportFragment.injectIfNeededIn(this);
}
```

ReportFragment 的 `injectIfNeededIn()` 是一個靜態方法,以 `android.app.Activity` 物件作為入參引數,此方法內部就會向 Activity 新增一個無 UI 介面的 ReportFragment

```kotlin
 public static void injectIfNeededIn(Activity activity) {
    if (Build.VERSION.SDK_INT >= 29) {
        // On API 29+, we can register for the correct Lifecycle callbacks directly
        activity.registerActivityLifecycleCallbacks(
                new LifecycleCallbacks());
    }
    //向 activity 新增一個不可見的 framework 中的 fragment,以此來取得 Activity 生命週期事件的回撥
    android.app.FragmentManager manager = activity.getFragmentManager();
    if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
        manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
        // Hopefully, we are the first to make a transaction.
        manager.executePendingTransactions();
    }
}
```

ReportFragment 就透過自身的各個回撥方法來間接獲得 Activity 生命週期事件的回撥通知

```java
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ···
    dispatch(Lifecycle.Event.ON_CREATE);
}

@Override
public void onStart() {
    super.onStart();
    ···
    dispatch(Lifecycle.Event.ON_START);
}

@Override
public void onDestroy() {
    super.onDestroy();
    dispatch(Lifecycle.Event.ON_DESTROY);
    ···
}
```

> 更多細節看這篇文章:[從原始碼看 Jetpack(1)-Lifecycle 原始碼詳解](https://juejin.cn/post/6847902220755992589)

### Glide

我們載入的圖片通常是要顯示在 ImageView 中的,而 ImageView 是會掛載在 Activity 或者 Fragment 等容器上的,當容器處於後臺或者已經被 finish 時,此時載入圖片的操作就應該被取消或者停止,否則也是在浪費寶貴的系統資源和網路資源,甚至可能發生記憶體洩露或者 NPE。那麼,顯而易見的一個問題就是,Glide 是如何判斷容器是否還處於活躍狀態的呢?

類似於 Jetpack 元件中的 Lifecycle 的實現思路,Glide 也是透過一個無 UI 介面的 Fragment 來間接獲取容器的生命週期狀態的

首先,Glide 包含一個 LifecycleListener,定義了三種事件通知回撥,用於通知容器的活躍狀態:是處於前臺、後臺、還是已經退出了。具體的實現類是 ActivityFragmentLifecycle

```java
public interface LifecycleListener {
  void onStart();
  void onStop();
  void onDestroy();
}
```

ActivityFragmentLifecycle 用於 SupportRequestManagerFragment 這個 Fragment 中來使用(省略了部分程式碼)。可以看到,在 Fragment 的三個生命週期回撥事件中,都會相應通知 ActivityFragmentLifecycle。那麼,不管 ImageView 的載體是 Activity 還是 Fragment,我們都可以向其注入一個無 UI 介面的 SupportRequestManagerFragment,以此來監聽載體在整個生命週期內活躍狀態的變化

```java
public class SupportRequestManagerFragment extends Fragment {
    
  private static final String TAG = "SupportRMFragment";
    
  private final ActivityFragmentLifecycle lifecycle;

  public SupportRequestManagerFragment() {
    this(new ActivityFragmentLifecycle());
  }

  @VisibleForTesting
  @SuppressLint("ValidFragment")
  public SupportRequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
    this.lifecycle = lifecycle;
  }

  @NonNull
  ActivityFragmentLifecycle getGlideLifecycle() {
    return lifecycle;
  }

  @Override
  public void onStart() {
    super.onStart();
    lifecycle.onStart();
  }

  @Override
  public void onStop() {
    super.onStop();
    lifecycle.onStop();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    lifecycle.onDestroy();
    unregisterFragmentWithRoot();
  }

  @Override
  public String toString() {
    return super.toString() + "{parent=" + getParentFragmentUsingHint() + "}";
  }
    
}
```

> 更多細節看這篇文章:[三方庫原始碼筆記(9)-Glide 原始碼詳解](https://juejin.cn/post/6891307560557608967)

## 2、方法代理

Fragment 還有一種特殊的用法。我們平時是透過 `requestPermissions()`、`onRequestPermissionsResult()` 等方法來申請許可權的,在一個地方發起許可權申請操作,在另一個特定方法裡拿到申請結果,整個流程是非同步且割裂開的。而 Fragment 包含了很多個和 Activity 完全一樣的 API,其中就包括這兩個許可權申請方法,因此我們可以透過自動向載體掛載一個無 UI 介面的 Fragment,讓 Fragment 來代理整個許可權申請流程,使得載體可以用流式申請的方式來完成整個操作

這裡提供一個簡單的小例子,程式碼很簡單,就不再過多講解了

```kotlin
/**
 * @Author: leavesCZY
 * @Desc:
 * @公眾號:位元組陣列
 */
class RequestPermissionsFragment : Fragment() {

    companion object {

        private const val EXTRA_PERMISSIONS = "github.leavesC.extra.PERMISSIONS"

        private const val REQUEST_CODE = 100

        fun request(
            activity: FragmentActivity,
            permissions: Array<String>,
            onRequestResult: ((Array<String>, IntArray) -> Unit)
        ) {
            val bundle = Bundle()
            bundle.putStringArray(EXTRA_PERMISSIONS, permissions)
            val requestPermissionsFragment = RequestPermissionsFragment()
            requestPermissionsFragment.arguments = bundle
            requestPermissionsFragment.onRequestResult = onRequestResult
            activity.supportFragmentManager.commit {
                setReorderingAllowed(true)
                addToBackStack(null)
                add(requestPermissionsFragment, null)
            }
        }

    }

    private val permissions by lazy {
        requireArguments().getStringArray(EXTRA_PERMISSIONS) ?: throw IllegalArgumentException()
    }

    private var onRequestResult: ((Array<String>, IntArray) -> Unit)? =
        null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        requestPermissions(permissions, REQUEST_CODE)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        onRequestResult?.invoke(permissions, grantResults)
        parentFragmentManager.popBackStack()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        onRequestResult = null
    }

}
```

之後在 FragmentActivity 或者 Fragment 中就可以透過以下方式來完成整個許可權申請操作了,直接在回撥裡拿到申請結果

```kotlin
RequestPermissionsFragment.request(
    fragmentActivity,
    arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.CAMERA
    )
) { permissions: Array<String>,
    grantResults: IntArray ->
    permissions.forEachIndexed { index, s ->
        showToast("permission:" + s + " grantResult:" + grantResults[index])
    }
}
```

![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05db3926efb84aedac886cb298ab40e0~tplv-k3u1fbpfcp-watermark.image)

# 9、結尾

自我感覺本篇文章已經講清楚了 Fragment 大部分的知識點了,陸陸續續寫了一萬多字,有基礎知識也有新知識,也許也包含了一些你還沒了解過的知識點,看完之後你有覺得 Fragment 如今真的在變得越來越好用了嗎 🤣🤣 有遺漏的知識點歡迎補充

最後當然也少不了本文的全部示例程式碼了:[AndroidOpenSourceDemo](https://github.com/leavesCZY/AndroidOpenSourceDemo)

相關文章