[譯][Google工程師] 剛剛發佈了 Fragment 的新特性 「Fragment 間傳遞數據的新方式」 以及源碼分析

就在 2020/05/07 號 Now in Android #17 更新了,發佈 Android 的新特性,其中就包括 Fragment 間通訊的新方式,你們能夠點擊這裏前往,看看都有那些更新java

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

  • 新 Fragment 間通訊的方式的使用?
  • 新 Fragment 間通訊的源碼分析?
  • 彙總 Fragment 之間的通訊的方式?

譯文

Frrgament 間傳遞數據能夠經過多種方式,包括使用 target Fragment APIs (Fragment.setTargetFragment() 和 Fragment.getTargetFragment()),ViewModel 或者 使用 Fragments’ 父容器 Activity,target Fragment APIs 已通過時了,如今鼓勵使用新的 Fragment result APIs 完成 Frrgament 之間傳遞數據,其中傳遞數據由 FragmentManager 處理,而且在 Fragments 設置發送數據和接受數據git

在 Frrgament 之間傳遞數據

使用新的 Fragment APIs 在 兩個 Frrgament 之間的傳遞,沒有任何引用,能夠使用它們公共的 FragmentManager,它充當 Frrgament 之間傳遞數據的中心存儲。github

接受數據

若是想在 Fragment 中接受數據,能夠在 FragmentManager 中註冊一個 FragmentResultListener,參數 requestKey 能夠過濾掉 FragmentManager 發送的數據算法

FragmentManager.setFragmentResultListener(
    requestKey,
    lifecycleOwner,
    FragmentResultListener { requestKey: String, result: Bundle ->
        // Handle result
    })
複製代碼

參數 lifecycleOwner 能夠觀察生命週期,當 Fragment 的生命週期處於 STARTED 時接受數據。若是監聽 Fragment 的生命週期,您能夠在接收到新數據時安全地更新 UI,由於 view 的建立(onViewCreated() 方法在 onStart() 以前被調用)。編程

當生命週期處於 LifecycleOwner STARTED 的狀態以前,若是有多個數據傳遞,只會接收到最新的值安全

當生命週期處於 LifecycleOwner DESTROYED 時,它將自動移除 listener,若是想手動移除 listener,須要調用 FragmentManager.setFragmentResultListener() 方法,傳遞空的 FragmentResultListenerbash

在 FragmentManager 中註冊 listener,依賴於 Fragment 發送返回的數據app

  • 若是在 FragmentA 中接受 FragmentB 發送的數據,FragmentA 和 FragmentB 處於相同的層級,經過 parent FragmentManager 進行通訊,FragmentA 必須使用 parent FragmentManager 註冊 listener
parentFragmentManager.setFragmentResultListener(...)
複製代碼
  • 若是在 FragmentA 中接受 FragmentB 發送的數據,FragmentA 是 FragmentB 的父容器, 他們經過 child FragmentManager 進行通訊
childFragmentManager.setFragmentResultListener(...)
複製代碼

listener 必須設置的Fragment 相同的 FragmentManageride

發送數據

若是 FragmentB 發送數據給 FragmentA,須要在 FragmentA 中註冊 listener,經過 parent FragmentManager 發送數據

parentFragmentManager.setFragmentResult(
    requestKey, // Same request key FragmentA used to register its listener
    bundleOf(key to value) // The data to be passed to FragmentA
)
複製代碼

測試 Fragment Results

測試 Fragment 是否成功接收或發送數據,能夠使用 FragmentScenario API

接受數據

若是在 FragmentA 中註冊 FragmentResultListener 接受數據,你能夠模擬 parent FragmentManager 發送數據,若是在 FragmentA 中正確註冊了 listener,能夠用來驗證 FragmentA 是否能收到數據,例如,若是在 FragmentA 中接受數據並更新 UI, 能夠使用 Espresso APIs 來驗證是否指望的數據

@Test
fun shouldReceiveData() {
    val scenario = FragmentScenario.launchInContainer(FragmentA::class.java)

    // Pass data using the parent fragment manager
    scenario.onFragment { fragment ->
        val data = bundleOf(KEY_DATA to "value")
        fragment.parentFragmentManager.setFragmentResult("aKey", data)
    }

    // Verify data is received, for example, by verifying it's been displayed on the UI onView(withId(R.id.textView)).check(matches(withText("value"))) } 複製代碼

發送數據

能夠在 FragmentB 的 parent FragmentManager 上註冊一個 FragmentResultListener 來測試 FragmentB 是否成功發送數據,當發送數據結束時,能夠來驗證這個 listener 是否能收到數據

@Test
fun shouldSendData() {
    val scenario = FragmentScenario.launchInContainer(FragmentB::class.java)

    // Register result listener
    var receivedData = ""
    scenario.onFragment { fragment ->
        fragment.parentFragmentManager.setFragmentResultListener(
            KEY,
            fragment,
            FragmentResultListener { key, result ->
                receivedData = result.getString(KEY_DATA)
            })
    }

    // Send data
    onView(withId(R.id.send_data)).perform(click())

    // Verify data was successfully sent
    assertThat(receivedData).isEqualTo("value")
}
複製代碼

示例項目

下面的示例項目,展現瞭如何使用 Fragment 新的 API

android-playground: https://github.com/husaynhakeem/android-playground...

總結

雖然使用了 Fragment result APIs,替換了過期的 Fragment target APIs,可是新的 APIs 在Bundle 做爲數據傳傳遞方面有一些限制,只能傳遞簡單數據類型、Serializable 和 Parcelable 數據,Fragment result APIs 容許程序從崩潰中恢復數據,並且不會持有對方的引用,避免當 Fragment 處於不可預知狀態的時,可能發生未知的問題

譯者的思考

這是譯者的一些思考,總結一下 Fragment 1.3.0-alpha04 新增長的 Fragment 間通訊的 API

數據接受

FragmentManager.setFragmentResultListener(
    requestKey,
    lifecycleOwner,
    FragmentResultListener { requestKey: String, result: Bundle ->
        // Handle result
    })
複製代碼

數據發送

parentFragmentManager.setFragmentResult(
    requestKey, // Same request key FragmentA used to register its listener
    bundleOf(key to value) // The data to be passed to FragmentA
)
複製代碼

那麼 Fragment 間通訊的新 API 給咱們帶來哪些好處呢:

  • 在 Fragment 之間傳遞數據,不會持有對方的引用
  • 當生命週期處於 ON_START 時開始處理數據,避免當 Fragment 處於不可預知狀態的時,可能發生未知的問題
  • 當生命週期處於 ON_DESTROY 時,移除監聽

咱們一塊兒來從源碼的角度分析一下 Google 是如何作的

源碼分析

按照慣例從調用的方法來分析,數據接受時,調用了 FragmentManager 的 setFragmentResultListener 方法
androidx.fragment/fragment/1.3.0-alpha04......androidx/fragment/app/FragmentManager.java

private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
        new ConcurrentHashMap<>();

@Override
public final void setFragmentResultListener(@NonNull final String requestKey,
                                            @NonNull final LifecycleOwner lifecycleOwner,
                                            @Nullable final FragmentResultListener listener) {
    // mResultListeners 是 ConcurrentHashMap 的實例,用來儲存註冊的 listener
    // 若是傳遞的參數 listener 爲空時,移除 requestKey 對應的 listener
    if (listener == null) {
        mResultListeners.remove(requestKey);
        return;
    }

    // Lifecycle是一個生命週期感知組件,通常用來響應Activity、Fragment等組件的生命週期變化
    final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
    // 當生命週期處於 DESTROYED 時,直接返回
    // 避免當 Fragment 處於不可預知狀態的時,可能發生未知的問題
    if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
        return;
    }

    // 開始監聽生命週期
    LifecycleEventObserver observer = new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                                   @NonNull Lifecycle.Event event) {
            // 當生命週期處於 ON_START 時開始處理數據
            if (event == Lifecycle.Event.ON_START) {
                // 開始檢查受到的數據
                Bundle storedResult = mResults.get(requestKey);
                if (storedResult != null) {
                    // 若是結果不爲空,調用回調方法
                    listener.onFragmentResult(requestKey, storedResult);
                    // 清除數據
                    setFragmentResult(requestKey, null);
                }
            }

            // 當生命週期處於 ON_DESTROY 時,移除監聽
            if (event == Lifecycle.Event.ON_DESTROY) {
                lifecycle.removeObserver(this);
                mResultListeners.remove(requestKey);
            }
        }
    };
    lifecycle.addObserver(observer);
    mResultListeners.put(requestKey, new FragmentManager.LifecycleAwareResultListener(lifecycle, listener));
}
複製代碼
  • Lifecycle是一個生命週期感知組件,通常用來響應Activity、Fragment等組件的生命週期變化
  • 獲取 Lifecycle 去監聽 Fragment 的生命週期的變化
  • 當生命週期處於 ON_START 時開始處理數據,避免當 Fragment 處於不可預知狀態的時,可能發生未知的問題
  • 當生命週期處於 ON_DESTROY 時,移除監聽

接下來一塊兒來看一下數據發送的方法,調用了 FragmentManager 的 setFragmentResult 方法
androidx.fragment/fragment/1.3.0-alpha04......androidx/fragment/app/FragmentManager.java

private final ConcurrentHashMap<String, Bundle> mResults = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
        new ConcurrentHashMap<>();
    
@Override
public final void setFragmentResult(@NonNull String requestKey, @Nullable Bundle result) {
    if (result == null) {
        // mResults 是 ConcurrentHashMap 的實例,用來存儲數據傳輸的 Bundle
        // 若是傳遞的參數 result 爲空,移除 requestKey 對應的 Bundle
        mResults.remove(requestKey);
        return;
    }

    // Check if there is a listener waiting for a result with this key
    // mResultListeners 是 ConcurrentHashMap 的實例,用來儲存註冊的 listener
    // 獲取 requestKey 對應的 listener
    LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
    if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
        // 若是 resultListener 不爲空,而且生命週期處於 STARTED 狀態時,調用回調
        resultListener.onFragmentResult(requestKey, result);
    } else {
        // 不然保存當前傳輸的數據
        mResults.put(requestKey, result);
    }
}
複製代碼
  • 獲取 requestKey 註冊的 listener
  • 當生命週期處於 STARTED 狀態時,開始發送數據
  • 不然保存當前傳輸的數據

源碼分析到這裏結束了,咱們一塊兒來思考一下,在以前咱們的都有那些數據傳方式

彙總 Fragment 之間的通訊的方式

  • 經過共享 ViewModel 或者關聯 Activity來完成,Fragment 之間不該該直接通訊 參考 Google: ViewModel#sharing
  • 經過接口,能夠在 Fragment 定義接口,並在 Activity 實現它 參考 Google: 與其餘 Fragment 通訊
  • 經過使用 findFragmentById 方法,獲取 Fragment 的實例,而後調用 Fragment 的公共方法 參考 Google: 與其餘 Fragment 通訊
  • 調用 Fragment.setTargetFragment() 和 Fragment.getTargetFragment() 方法,可是注意 target fragment 須要直接訪問另外一個 fragment 的實例,這是十分危險的,由於你不知道目標 fragment 處於什麼狀態
  • Fragment 新的 API, setFragmentResult() 和 setFragmentResultListener()

綜合以上通訊方式,那麼你認爲 Fragment 之間通訊最好的方式是什麼?

參考文獻

結語

致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯相關的文章,目前正在翻譯一系列歐美精選文章,不單單是翻譯,還有翻譯背後對每篇文章思考,若是你喜歡這片文章,請幫我點個贊,感謝,期待與你一塊兒成長

文章列表

Android 10 源碼系列

工具系列

逆向系列

相關文章
相關標籤/搜索