Fragment 這些 API 已廢棄,你還在使用嗎?

語言: CN / TW / HK

theme: devui-blue highlight: a11y-light


引言

Fragment 誕生之初就被定義為一個小型 Activity,因此它代理了 Activity 的許多能力(例如 startActivityForResult 等),職責不夠單一。隨著 Jetpack 各種新元件的出現,Fragment 的很多職責被有效地進行了分擔,其本身也可以更更好地聚焦在對 UI 的劃分而管理上面,以前的一些 API 也可以退出歷史舞臺了。本文就盤點一下 Fragment 那些被廢棄的 API。

本文的介紹基於 Fragment 版本 1.4.0

1. instantiate

以前, Fragment 的建構函式不允許攜帶引數,因為某些場景中 Fragment 會由系統自動建立,例如基於 XML 建立 Fragment、Activity 被殺死後的恢復重建等等。此時,系統通過呼叫 instantiate 來建立 Fragment,instantiate 通過反射呼叫 Fragment 無參的建構函式。

現在 Fragment 的建構函式允許攜帶引數了,我們可以通過自定義 FragmentFactory,呼叫 Fragment 的任意建構函式,而系統通過呼叫 FragmentFactory 來建立 Fragment。

我們可以自定義 FragmentFactory,並重寫它的 instantiate 方法來建立 Fragment:

```kotlin class MyFragmentFactory(private val arg: Any) : FragmentFactory() {

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

} ```

我們將 FragmentFactory 設定給 FragmentManger,之後系統就可以在各種場景中使用工廠建立 Fragment 了。

```kotlin //Activity override fun onCreate(savedInstanceState: Bundle?) {

supportFragmentManager.fragmentFactory = myFragmentFactory
super.onCreate(savedInstanceState)

} ``` 注意 FragmentFactory 的設定必須在 super.onCreate 之前,因為當 Activity 進入重建路徑時,會在 super.onCreate 中使用到它。

關於 FragmentFactory 的更多介紹:https://juejin.cn/post/6989048326633029645

2. onActivityCreated

Fragment 早期設計中與 Activity 耦合較多,例如在生命週期方面上除了代理了 Activity 標準生命週期回撥以外,還增加了 onActivityCreated 用來觀察與 Activity 的繫結關係,onActivityCreated 被認為是 onStart 之前最後一個階段,此時 Fragment 的 View Hierarchy 已經與 Activity 繫結,因此常用來在這裡完成一些基於 View 的初始化工作。

現在,官方正在逐漸去掉 Fragment 與 Activity 之間的耦合,一個更加獨立的 Fragment 更利於複用和測試,因此 onActivityCreated 被廢除,取而代之的是在 onViewCreated 中處理與 View 相關的初始化邏輯,與 View 無關的初始化可以前置到 onCreate。但要注意 onViewCreated 回撥的時間點,Fragment 的 View 還沒加入 Activity View 的 Hierarchy。

如果我們實在需要獲得 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) //... } }) } onAttach(Context) 是 API23 之後新增的 API,前身是 onAttach(Activity),它也是為了去掉與 Activity 的耦合而被廢棄和取代。

關於 Fragment 生命週期的更多內容:https://juejin.cn/post/7006970844542926855#heading-3

3. setRetainInstance

當系統發生橫豎屏旋轉等 ConfigurationChanged 時,伴隨 Activity 的重新 onCreate,Fragment 也會重新建立。setRetainInstance(true) 可以保持 ConfigurationChanged 之後的 Fragment 例項不變。因為有這個特性,以前我們經常會藉助 setRetainInstance 來儲存 Fragment 甚至 Activity 的狀態。

但是使用 setRetainInstance 儲存狀態存在隱患,如果 Fragment 持有了對 Activity View 的引用則會造成洩露或者異常,所以我們僅儲存與 View 無關的狀態即可,不應該儲存整個 Fragment 例項,所以 setRetainInstance/getRetainInstance 被廢棄,取而代之的是推薦使用 ViewModel 儲存狀態。

對於 ViewModel 的基操想必大家都很熟悉就不贅述了。這裡只提醒一點,既然 ViewModel 可以在 ConfigurationChanged 之後保持狀態,那麼 ViewModel 的初始化只需進行一次即可。不少人會像下面這樣初始化 ViewModel

```kotlin class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){

private val viewModel : DetailTaskViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    //訂閱 ViewModel
    viewMode.uiState.observe(viewLifecycleOwner) {
       //update ui
    }

    //請求資料
    viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))
}

} `` 在 onViewCreated 中使用fetchTaskData請求資料,當橫豎屏旋轉造成 Fragment 重建時,雖然我們可以從 ViewModel 中獲取最新資料,但是仍然會執行一次多餘的 fetchTaskData 。因此更合理的 ViewModel 初始化時機應該是在其內部的init` 中進行,程式碼如下:

```kotlin class TasksViewModel: ViewModel() {

private val _tasks = MutableLiveData<List<Task>>()
val tasks: LiveData<List<Task>> = _uiState

init {
    viewModelScope.launch {
        _tasks.value = withContext(Dispatchers.IO){
            TasksRepository.fetchTasks()
        }
    }
}

} ```

關於 ViewModel 初始化時機的相關內容,請參考:https://juejin.cn/post/6997213075875037214

4. setUserVisibleHint

Fragment 經常配合 ViewPager 使用以滿足多 Tab 頁場景的需求。預設情況下螢幕外部的 Fragment 會跟隨顯示中的 Fragment 一同被載入,這會影響初始頁面的顯示速度。setUserVisibleHint 是以前我們常用的“懶載入”實現方案:當 ViewPager 中的 Fragment 進/出螢幕時,FragmentPagerAdapter 會對其呼叫 setUserVisibleHint,傳入 true/false,通知其是否可見:

java @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser) { onVisible(); //自定義回撥: 進入螢幕 } else { onInVisible();//離開螢幕 } }

如上,通過重寫 setUserVisibleHint 我們可以在 onVisible/onInVisible 中獲知 Fragment 顯示的時機,便於實現懶載入。但是這種做法有缺陷,首先,你需要為 Fragment 增加基類來定義 onVisible/onInvisible,其次,新增的這兩個方法跟原生的生命週期回撥交織在一起,增加了程式碼複雜度和出錯的概率。幸好現在我們有了新的“懶載入”解決方案: FragmentTransaction#setMaxLifecycle:setMaxLifecycle 可以將螢幕外尚未顯示的 Fragment 的最大的生命週期的狀態限制在 Started

當 Fragment 真正進入屏幕後再推進到 Resumed,此時 onResume 才會響應。藉助 setMaxLifecycle 我們僅依靠原生回撥即可實現懶載入,而且還避免了額外基類的引入。

如果你使用的是 ViewPager2,其對應的 FragmentStateAdapter 已經預設支援了 setMaxLifecycle 。對於傳統的 ViewPager,啟動 setMaxLifecycle 的方法也很簡單,FragmentPagerAdapter 的構造方法新增了一個 behavior 引數, 只要在此處傳值為 FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 即可,在 instantiateItem 方法中,會根據 behavior 為建立的 Fragment 設定 setMaxLifecycle。

```java // FragmentPagerAdpater.java @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { ... if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); // mBehaviour為1的時候走新邏輯if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { // 初始化item時將其生命週期限制為STARTED mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } else { // 相容舊版邏輯 fragment.setUserVisibleHint(false); } }

return fragment;

} ```

關於 setMaxLifecycle 的工作原理:https://juejin.cn/post/6961638025876996126

5. onActivityResult

以前,我們在 Fragment 可以通過 startActivityForResult/onActivityResult 啟動 Activity 並獲取返回的結果,這本質是呼叫了 Activity 的同名方法。隨著 Activity Result API 的啟用,startActivityForResult/onActivityResult 已經在 Activity 以及 Fragment 中被廢棄。相對於 onActivityResult 的結果返回方式,Activity Result API 避免了對 requestCode 的依賴,以更加直觀的方式獲得 Activity 返回結果。

基本使用步驟如下圖:

首先,我們建立一個 ActivityResultContract,這裡定義了跨 Activity 通訊的輸入輸出協議,系統預置了一系列 ActivityResultContracts.XXXX 可直接使用。然後,我們使用 registerForActivityResult 註冊我們的 Contract 和對應的 Callback,Callback 中我們可以獲取 Activity 的返回結果。程式碼如上

kotlin val launcher : ActivityResultLauncher = registerForActivityResult( //使用預置的 Contract:StartActivityForResult ActivityResultContracts.StartActivityForResult()) { activityResult -> // 獲取 Activity 返回的 ActivityResult Log.d("TargetActivity", activityResult.toString()) // D/TargetActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }} } registerForActivityResult 會返回一個 ActivityResultLauncher 控制代碼,我們使用它啟動 Activity,如下: kotlin val intent = Intent(this, TargetActivity::class.java) launcher.launch(intent)

最後我們在目標 Activity 中呼叫 setResult 返回結果即可: ```kotlin //TargetActivity.kt class TargetActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data"))
    finish()
}

} ```

關於 Activity Result API 的更多底層原理可以參考 https://juejin.cn/post/6922866182190022663

6. requestPermissions

requestPermissions/onRequestPermissionsResult 底層也是基於 startActivityForResult/onActivityResult 實現的,因此同樣被廢棄了,升級為 Result API 的方式。

ActivityResultContracts 預置了申請許可權相關的 Contract:

```kotlin request_permission.setOnClickListener { requestPermission.launch(permission.BLUETOOTH) }

request_multiple_permission.setOnClickListener { requestMultiplePermissions.launch( arrayOf( permission.BLUETOOTH, permission.NFC, permission.ACCESS_FINE_LOCATION ) ) }

// 申請單一許可權 private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> // Do something if permission grantedif (isGranted) toast("Permission is granted") else toast("Permission is denied") }

// 一次申請多許可權 private val requestMultiplePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions : Map -> // Do something if some permissions granted or denied permissions.entries.forEach { // Do checking here }
} ```

7. setTargetFragment

setTargetFragment/getTargetFragment 原本用於 Fragment 之間的通訊,例如從 FragmentA 跳轉到 FragmentB ,在 B 中傳送結果返回給 A:

```java // 向 FragmentB 設定 targetFragment FragmentB fragment = new FragmentB(); fragment.setTargetFragment(FragmentA.this, AppConstant.REQ_CODE_SECOND_FRAGMENT);

//切換至 FragmentB transaction.replace(R.id.fragment_container, fragment).commit();

// FragmentB 中獲取 FragmentA 並進行回撥 Fragment fragment = getTargetFragment(); fragment.onActivityResult(AppConstant.REQ_CODE_SECOND_FRAGMENT, Activity.RESULT_OK, inte ```

如上,程式碼非常簡單,但是這樣的通訊無法感應生命週期,即使 FragmentA 處於後臺也會在 onActivityResult 響應回撥。目前 TargetFragment 相關 API 已經被廢棄,取而代之的是根為合理的 Fragment Result API

假設需要在 FragmentA 監聽 FragmentB 返回的資料,首先在 FragmentA 設定監聽

```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // setFragmentResultListener 是 fragment-ktx 提供的擴充套件函式 setFragmentResultListener("requestKey") { requestKey, bundle -> // 監聽key為“requestKey”的結果, 並通過bundle獲取 val result = bundle.getString("bundleKey") // ... } }

// setFragmentResultListener 是Fragment的擴充套件函式,內部呼叫 FragmentManger 的同名方法 public fun Fragment.setFragmentResultListener( requestKey: String, listener: ((requestKey: String, bundle: Bundle) -> Unit) ) { parentFragmentManager.setFragmentResultListener(requestKey, this, listener) } ```

當從 FragmentB 返回結果時:

```kotlin val result = "result" setFragmentResult("requestKey", bundleOf("bundleKey" to result))

//setFragmentResult 也是 Fragment 的擴充套件函式,其內部呼叫 FragmentManger 的同名方法 public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) { parentFragmentManager.setFragmentResult(requestKey, result) } ```

上面的程式碼可以用下圖表示:

FragmentA 通過 Key 向 FragmentManager 註冊 ResultListener,FragmentB 返回 result 時, FM 通過 Key 將結果回撥給FragmentA ,而且最重要的是 Result API 是生命週期可感知的,listener.onFragmentResultLifecycle.Event.ON_START 的時候才呼叫,也就是說只有當 FragmentA 返回到前臺時,才會收到結果。

關於 Fragment Result API 的更多介紹,可以參考: https://juejin.cn/post/6977241599344377863

最後

Fragment 是幫助我們組織和管理 UI 的重要元件,即使在 Compose 時代也具有使用價值,因此谷歌官方一直致力於對它的 API 的優化,希望他更加易用和便於測試。這些已廢棄的 API 在未來的版本中將會徹底刪除,所以如果你還在使用著他們,應該儘快予以替換。

官方也提供了工具幫助我們發現對於過期 API 的使用,Fragment-1.4.0 之後,我們可以通過全域性設定嚴格模式策略,發現專案中的問題:

```kotlin class MyApplication : Application() {

override fun onCreate() {
    super.onCreate()

    FragmentStrictMode.defaultPolicy =
        FragmentStrictMode.Policy.Builder()
            .detectFragmentTagUsage() //setTargetFragment的使用
            .detectRetainInstanceUsage()//setRetainInstance的使用
            .detectSetUserVisibleHint()//setUserVisibleHint的使用
            .detectTargetFragmentUsage()//setTargetFragment的使用
            .apply {
                if (BuildConfig.DEBUG) {
                    // Debug 模式下崩潰
                    penaltyDeath()
                } else {
                    // Release 模式下上報
                    penaltyListener {
                        FirebaseCrashlytics.getInstance().recordException(it)
                    }
                }
            }
            .build()
}

} ```

關於 FragmentStrictMode 的更多內容,請參考:https://developer.android.com/guide/fragments/debugging#strictmode

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿