[譯][Google工程師] 詳解 FragmentFactory 如何優雅使用 Koin 以及部分源碼分析

前言

在以前的文章 [譯][Google工程師] 剛剛發佈了 Fragment 的新特性 「Fragment 間傳遞數據的新方式」 以及源碼分析 介紹了 Fragment 1.3.0 中添加了幾個重要的 API。html

繼續上一篇文章,介紹一下 FragmentFactory 和 FragmentContainerView 以及如何和 Koin 一塊兒使用, 這是 Google 在 Fragment 1.2.0 上作的重要的更新,強烈建議你們去使用java

經過這篇文章你將學習到如下內容,將在譯者思考部分會給出相應的答案android

  • FragmentFactory 是什麼?
  • 什麼狀況下使用 FragmentFactory?
  • FragmentContainerView 是什麼?
  • 爲何 Google 強烈建議使用 FragmentContainerView?
  • Koin 如何和 FragmentFactory 一塊兒使用以及部分源碼分析?
  • 如何處理嵌套 Fragment?

這篇文章涉及不少重要新的知識點,帶着本身理解,請耐心讀下去,應該能夠從中學到不少技巧。git

譯文

如今咱們可使用 FragmentFactory 來完成 Fragment 構造函數的注入,可是這不是開發人員必須使用的 API, 在某些狀況下,它能夠被認爲是一種很好的設計方法,幫助咱們測試帶有外部依賴項的 Fragment。github

這篇文章將會解釋什麼是 FragmentFactory,何時以及如何使用它,如何處理嵌套 Fragment。算法

什麼是 FragmentFactory?

以前 Fragment 的實例都是經過使用默認的空的構造函數進行實例化,這是由於系統須要在某些狀況下從新初始化它,好比配置更改或者 App 的進程重建,若是沒有默認構造函數的限制,系統不知道如何從新初始化 Fragment 實例。編程

FragmentFactory 出現就是爲了解決這個限制,經過向其提供實例化 Fragment 所需的參數/依賴關係,FragmentFactory 能夠幫助系統建立 Fragment 實例。性能優化

如何使用 FragmentFactory?

若是你的 Fragment 沒有空的構造函數,您須要建立一個 FragmentFactory 來處理初始化它,經過繼承 FragmentFactory 而且覆蓋 FragmentFactory#instantiate() 來完成。bash

class CustomFragmentFactory(private val dependency: Dependency) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        if (className == CustomFragment::class.java.name) {
            return CustomFragment(dependency)
        }
        return super.instantiate(classLoader, className)
    }
}
複製代碼

Fragment 是由 FragmentManagers 來管理的,因此爲了使用 FragmentFactory 須要關聯 FragmentManager,更具體的說它必須分配給包含 Fragment 組件的 FragmentManager,它能夠是 Activity 或者 Fragment。app

何時 FragmentFactory 和 FragmentManager 作關聯

FragmentFactory 負責在 Activity 和 parent Fragment 初始化 Fragment,因此應該在建立 Fragment 以前設置它。

  • 在建立 component’s View 以前:若是在 XML 中定義 Fragment,應該使用 Fragment 的 tag <fragment> 或者 FragmentContainerView。
  • 在建立 Fragment 以前:若是 Fragment 是動態添加的應該使用 FragmentTransaction。
  • 在系統恢復 Fragment 以前:若是是由於配置更改或者 App 的進程重建,致使 Fragment 重建。

有了這些限制,能夠在 Activity#onCreate()Fragment#onCreate() 以前關聯 FragmentFactory 和 FragmentManager,在這兩個調用處 view 建立以前會從新初始化 Fragment。

這也就意味着應該在 super#onCreate() 以前關聯 FragmentFactory 和 FragmentManager。

  • 在 Activity 關聯 FragmentFactory 和 FragmentManager
class HostActivity : AppCompatActivity() {
    private val customFragmentFactory = CustomFragmentFactory(Dependency())

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = customFragmentFactory
        super.onCreate(savedInstanceState)
        // ...
    }
}
複製代碼
  • 在 Fragment 關聯 FragmentFactory 和 FragmentManager
class ParentFragment : Fragment() {
    private val customFragmentFactory = CustomFragmentFactory(Dependency())

    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = customFragmentFactory
        super.onCreate(savedInstanceState)
        // ...
    }
}
複製代碼

須要使用 FragmentFactory 嗎?

到目前爲止,您可能已經使用它們的默認構造函數建立 Fragment,而後使用 Dagger 或 Koin 這樣的庫注入它們須要的依賴項,或者在它們被使用以前在 Fragment 中初始化它們。

若是你的 Fragment 有一個默認的空構造函數,那麼就不須要使用 FragmentFactory,若是在 Fragment 構造函數中接受參數,必須使用 FragmentFactory,否者會拋出 Fragment.InstantiationException 異常

如何同時使用 Fragment 和 FragmentFactory?

只須要在建立 Fragment 以前,設置 FragmentFactory,它就會被用來實例化,這意味着在添加 Fragments 以前使用自定義的 FragmentFactory。

  • 靜態添加: 使用 Fragment 的 tag <fragment> 和 FragmentContainerView。
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/customFragment"
    android:name="com.example.CustomFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="custom_fragment" />
複製代碼

設置 FragmentFactory 用於初始化在Fragment 聲明的 FragmentContainerView

class HostActivity : AppCompatActivity() {
    private val customFragmentFactory = CustomFragmentFactory(Dependency())

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = customFragmentFactory
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_with_fragment_container_view)
    }
}
複製代碼
  • 動態添加: 使用 FragmentTransaction#add() 方法動態的添加 Fragment
class HostActivity : AppCompatActivity() {
    private val customFragmentFactory = CustomFragmentFactory(Dependency())

    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = customFragmentFactory
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_with_empty_frame_layout)
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .add(R.id.content, CustomFragment::class.java, arguments)
                .commit()
        }
    }
}
複製代碼

FragmentFactory 和嵌套的 Fragment

若是 parent Fragment 包含嵌套的 Fragment 或者多層次嵌套的 Fragment,它們都會使用 parent Fragment 的相同 FragmentFactory,嵌套 Fragment 須要調用 Fragment#childFragmentManager.fragmentFactory

class ParentFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = parentFragmentFactory
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            // Add NestedFragment
        }
    }
}

class NestedFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        childFragmentManager.fragmentFactory = childFragmentFactory
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            // Add NestedNestedFragment
        }
    }
}

class NestedNestedFragment : Fragment()
複製代碼

譯者思考

咱們來總結一下 Fragment 幾個重要的更新,以及在什麼狀況下使用:

  • 以前 Fragment 的實例都是經過使用默認的空的構造函數進行實例化的,FragmentFactory 出現就是爲了解決這個限制。
  • FragmentFactory 不是必需要使用的,若是在 Fragment 構造函數中接受參數,必須使用 FragmentFactory
  • FragmentFactory 須要在 Activity 或者 Fragment 中使用,而且須要在 Activity#onCreate()Fragment#onCreate() 以前和 FragmentManager 作關聯
  • 嵌套的 Fragment 或者多層次嵌套的 Fragment,使用的是相同 FragmentFactory
  • 正由於 FragmentFactory 出現,能夠在 Fragment 構造函數中傳遞參數,意味着可使用 Koin 等框架,能夠實現構造函數依賴注入,後面我會演示如何使用

接下來一塊兒瞭解一下什麼 FragmentContainerView,爲何 Google 強烈建議使用 FragmentContainerView 容器來存儲動態添加的 Fragment。

FragmentContainerView 是什麼?爲何 Google 強烈建議使用?

咱們先來看一下 Google 的更新說明:

FragmentContainerView: FragmentContainerView 是一個自定義 View 繼承 FrameLayout,與 ViewGroups 不一樣,它只接受 Fragment Views。

爲何 Google 強烈建議使用?

以前在 Google issue 提了一個 fragment z-ordering 的問題,就是說 Fragment 進入和退出動畫會致使一個問題,進入的 Fragment 會在退出的 Fragment下面,直到它徹底退出屏幕,這會致使在 Fragment 之間切換時產生錯誤的動畫。

使用 FragmentContainerView 帶來的好處是改進了對 fragment z-ordering 的處理。這是 Google 演示的例子,優化了兩個 Fragment 退出和進入過渡不會互相重疊,使用 FragmentContainerView 將先開啓退出動畫而後纔是進入動畫。

Koin 如何和 FragmentFactory 一塊兒使用以及源碼分析

在以前的文章 [譯][2.4K Start] 放棄 Dagger 擁抱 Koin 分析了 Koin 性能,若是沒有看過,建議能夠去了解一下。

Koin 團隊在 2.1.0 版本開始支持 Fragment 的依賴注入,截圖以下所示:

1. 添加 Koin Fragment 依賴

implementation "org.koin:koin-androidx-fragment:2.1.5"
複製代碼

2. 建立 Fragment 並傳遞 ViewModel

class FragmentTest(val mainViewModel: MainViewModel) : Fragment(){
    ......
}
複製代碼

3. 建立 Fragment modules

val viewModelsModule = module {
    viewModel { MainViewModel() }
}

val fragmentModules = module {
    fragment { FragmentTest(get()) }
}

val appModules = listOf(fragmentModules, viewModelsModule)
複製代碼

4. 在調用 startKoin 方法時設置 KoinFragmentFactory

startKoin {
    AndroidLogger(Level.DEBUG)
    androidContext(this@App)
    fragmentFactory()
    loadKoinModules(appModules)
}
複製代碼

fragmentFactory 是 KoinApplication 的擴展函數,提供了 KoinFragmentFactory 代碼以下所示:

koin.loadModules(listOf(module {
        single<FragmentFactory> { KoinFragmentFactory() }
    }))
}
複製代碼

一塊兒來分析 KoinFragmentFactory 內部的源碼:

class KoinFragmentFactory(val scope: Scope? = null) : FragmentFactory(), KoinComponent {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val clazz = Class.forName(className).kotlin
        val instance = if (scope != null) {
            scope.getOrNull<Fragment>(clazz)
        }else{
            getKoin().getOrNull<Fragment>(clazz)
        }
        return instance ?: super.instantiate(classLoader, className)
    }

}
複製代碼

繼承 FragmentFactory 而且重寫了 FragmentFactory#instantiate() 方法,在這個函數中,咱們使用 className 做爲參數獲取 Fragment,並嘗試從 Koin 中檢索 Fragment 實例

5. 在 onCreate 方法以前 調用 setupKoinFragmentFactory 綁定 FragmentFactory

override fun onCreate(savedInstanceState: Bundle?) {
    setupKoinFragmentFactory()
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}
複製代碼

6. 添加 Fragment 並傳遞 Bundle

val arguments = Bundle().apply {
    putString(FragmentTest.KEY_NAME, "來源於 MainActivity")
}

supportFragmentManager.beginTransaction()
    .replace(R.id.container, FragmentTest::class.java, arguments)
    .commit()
複製代碼

相關源碼已經上傳到 JDataBinding 中, 能夠查看 App、MainActivity、AppModule 和 FragmentTest 這幾個類

參考文獻

結語

致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯相關的文章,目前正在翻譯一系列歐美精選文章,請持續關注,除了翻譯還有對每篇歐美文章思考,若是對你有幫助,請幫我點個贊,感謝!!!期待與你一塊兒成長。

文章列表

Android 10 源碼系列

Android 應用系列

工具系列

逆向系列

相關文章
相關標籤/搜索