一道面試題:介紹一下 Fragment 間的通訊方式?
highlight: androidstudio theme: smartblue
這是我參與更文挑戰的第8天,活動詳情檢視: 更文挑戰
Fragment 間的通訊可以藉助以下幾種方式實現:
- EventBus
- Activity(or Parent Fragment)
- ViewModel
- Result API
1. 基於 EventBus 通訊
EventBus 的優缺點都很突出。 優點是限制少可隨意使用,缺點是限制太少使用太隨意。
因為 EventBus 會導致開發者在架構設計上“不思進取”,隨著專案變複雜,結構越來越混亂,程式碼可讀性變差,資料流的變化難以追蹤。
所以,規模越大的專案 EvenBus 的負面效果越明顯,因此很多大廠都禁止 EventBus 的使用。所以這道題千萬不要把 EventBus 作為首選答案,比較得體的回答是:
“ EventBus 具備通訊能力,但是缺點很突出,大量使用 EventBus 會造成專案難以維護、問題難以定位,所以我不建議在專案中使用 EventBus 進行通訊。 ”
2. 基於 Activity 或父 Fragment 通訊
為了迭代更加敏捷,Fragment 從 AOSP 遷移到了 AndroidX ,這導致同時存在著兩種包名的 Fragment:android.app.Fragment
和 andoridx.fragment.app.Fragment
。
雖然前者已經被廢棄,但很多歷史程式碼中尚存, 對於老的Fragment,經常依賴基於 Activity 的通訊方式,因為其他通訊方式大都依賴 AndroidX 。
```kotlin class MainActivity : AppCompatActivity() {
val listFragment: ListFragment by lazy {
ListFragment()
}
val CreatorFragment: CreatorFragment by lazy {
// 構建Fragment的時候設定 Callback,建立通訊
CreatorFragment().apply {
setOnItemCreated {
listFragment.addItem(it)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().apply {
add(R.id.fragmentContainer, listFragment)
commit()
}
}
} ``` 如上,在 Activity 或父 Fragment 中建立子Fragment,同時為其設定 Callback
此時,Fragment 的建立依賴手動配置,無法在 ConfigurationChangeed 的時候自動恢復重建,所以除了用來處理 android.app.Fragment
的歷史遺留程式碼之外,不推薦使用。
3. 基於 ViewModel 通訊
ViewModel 是目前使用最廣泛的通訊方式之一,在 Kotlin 中使用時,需要引入fragment-ktx
```kotlin
class ListViewModel : ViewModel() {
private val originalList: LiveData>() = ...
val itemList: LiveData
> = ...
fun addItem(item: Item) {
//更新 LiveData
}
}
class ListFragment : Fragment() { // 藉助ktx,使用activityViewModels()代理方式獲取ViewModel private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.itemList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI } } }
class CreatorFragment : Fragment() { private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
val item = ...
viewModel.addItem(item)
}
}
}
``` 如上,通過訂閱 ViewModel 的 LiveData,接受資料變通的通知。因為兩個 Fragment 需要共享ViewModel,所以 ViewModel 必須在 Activity 的 Scope 中建立
關於 ViewModel 的實現原理,相關文章很多,本文不做贅述了。接下來重點看一下 Result API:
4. 基於 Resutl API 通訊
從Fragment 1.3.0-alpha04
起,FragmentManager 新增了 FragmentResultOwner
介面,顧名思義 FragmentManager 成為了 FragmentResult 的持有者,可以進行 Fragment 之間的通訊。
假設需要在 FragmentA 監聽 FragmentB 返回的資料,首先在 FragmentA 設定監聽 ```java 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 返回結果時: ```java button.setOnClickListener { val result = "result" setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
//setFragmentResult 也是 Fragment 的擴充套件函式,其內部呼叫 FragmentManger 的同名方法 public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) { parentFragmentManager.setFragmentResult(requestKey, result) } ```
上面的程式碼可以用下圖表示:
Result API的原理非常簡單,FragmentA 通過 Key 向 FragmentManager 註冊 ResultListener
,FragmentB 返回 result 時, FM 通過 Key 將結果回撥給FragmentA 。需要特別注意的是隻有當 FragmentB 返回時,result才會被真正回傳,如果 setFragmentResult
多次,則只會保留最後一次結果。
生命週期可感知
通過梳理原始碼可以知道Result API是LifecycleAware的
原始碼基於 androidx.fragment:fragment:1.3.0
setFragmentResultListener 實現:
```java
//FragmentManager.java
private final Map
public final void setFragmentResultListener(@NonNull final String requestKey, @NonNull final LifecycleOwner lifecycleOwner, @NonNull final FragmentResultListener listener) { final Lifecycle lifecycle = lifecycleOwner.getLifecycle(); LifecycleEventObserver observer = new LifecycleEventObserver() { if (event == Lifecycle.Event.ON_START) { // once we are started, check for any stored results Bundle storedResult = mResults.get(requestKey); if (storedResult != null) { // if there is a result, fire the callback listener.onFragmentResult(requestKey, storedResult); // and clear the result clearFragmentResult(requestKey); } }
if (event == Lifecycle.Event.ON_DESTROY) {
lifecycle.removeObserver(this);
mResultListeners.remove(requestKey);
}
};
lifecycle.addObserver(observer);
LifecycleAwareResultListener storedListener = mResultListeners.put(requestKey,
new LifecycleAwareResultListener(lifecycle, listener, observer));
if (storedListener != null) {
storedListener.removeObserver();
}
}
```
-
listener.onFragmentResult
在Lifecycle.Event.ON_START
的時候才呼叫,也就是說只有當 FragmentA 返回到前臺時,才會收到結果,這與 LiveData 的邏輯的行為一致,都是 LifecycleAware 的 -
當多次呼叫
setFragmentResultListener
時, 會建立新的LifecycleEventObserver
物件, 同時舊的 observer 會隨著storedListener.removeObserver()
從 lifecycle 中移除,不能再被回撥。
也就是說,對於同一個 requestKey 來說,只有最後一次設定的 listener 有效,這好像也是理所應當的,畢竟不叫 addFragmentResultListener
。
setFragmentResult 實現:
```java
private final Map
public final void setFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
// Check if there is a listener waiting for a result with this key
LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
// if there is and it is started, fire the callback
if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
resultListener.onFragmentResult(requestKey, result);
} else {
// else, save the result for later
mResults.put(requestKey, result);
}
}
``
setFragmentResult非常簡單, 如果當前是 listener 處於前臺,則立即回撥
setFragmentResult(), 否則,存入
mResults`, 等待 listener 切換到前臺時再回調。
一個 listener 為什麼有前臺/後臺的概念呢,這就是之前看到的 LifecycleAwareResultListener
了, 生命週期可感知是因為其內部持有一個 Lifecycle
, 而這個 Lifecycle 其實就是設定 listener 的那個 Fragment
```java private static class LifecycleAwareResultListener implements FragmentResultListener { private final Lifecycle mLifecycle; private final FragmentResultListener mListener; private final LifecycleEventObserver mObserver;
LifecycleAwareResultListener(@NonNull Lifecycle lifecycle,
@NonNull FragmentResultListener listener,
@NonNull LifecycleEventObserver observer) {
mLifecycle = lifecycle;
mListener = listener;
mObserver = observer;
}
public boolean isAtLeast(Lifecycle.State state) {
return mLifecycle.getCurrentState().isAtLeast(state);
}
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
mListener.onFragmentResult(requestKey, result);
}
public void removeObserver() {
mLifecycle.removeObserver(mObserver);
}
}
```
可恢復重建
mResult
中的資料是會隨著 Fragment 的重建可以恢復的,所以 FragmentA 永遠不會丟失 FragmentB 返回的結果。當然,一旦 Result 被消費,就會從 mResult
中清除
mResults 的儲存
java
//FragmentManager.java
void restoreSaveState(@Nullable Parcelable state) {
//...
ArrayList<String> savedResultKeys = fms.mResultKeys;
if (savedResultKeys != null) {
for (int i = 0; i < savedResultKeys.size(); i++) {
mResults.put(savedResultKeys.get(i), fms.mResults.get(i));
}
}
}
mResults 的恢復
java
Parcelable saveAllState() {
// FragmentManagerState implements Parcelable
FragmentManagerState fms = new FragmentManagerState();
//...
fms.mResultKeys.addAll(mResults.keySet());
fms.mResults.addAll(mResults.values());
//...
return fms;
}
如何選擇?Result API 與 ViewModel
ResultAPI 與 ViewModel + LiveData 有一定相似性,都是生命週期可感知的,都可以在恢復重建時儲存資料,那這兩種通訊方式該如何選擇呢?
對此,官方給的建議如下:
The Fragment library provides two options for communication: a shared ViewModel and the Fragment Result API. The recommended option depends on the use case. To share persistent data with any custom APIs, you should use a ViewModel. For a one-time result with data that can be placed in a Bundle, you should use the Fragment Result API.
-
ResultAPI 主要適用於那些一次性的通訊場景(FragmentB返回結果後結束自己)。如果使用 ViewModel,需要上提到的 Fragment 共同的父級 Scope,而 Scope 的放大不利於資料的管理。
-
非一次性的通訊場景,由於 FragmentA 和 FragmentB 在通訊過程中共存,推薦通過共享 ViewModel 的方式,再借助 LiveData 等進行響應式通訊。
5. 跨Activity的通訊
最後看一下,跨越不同 Activity 的 Fragmnet 間的通訊
跨 Activity 的通訊主要有兩種方式:
- startActivityResult
- Activity Result API
startActivityResult
Result API出現之前,需要通過 startActivityResult
完成通訊,這也是 android.app.Fragment
唯一可選的方式。
通訊過程如下:
-
FragmentA 呼叫 startActivityForResult() 方法之後,跳轉到 ActivityB 中,ActivityB 把資料通過
setArguments()
設定給 FragmentB -
FragmentB 呼叫
getActivity().setResult()
設定返回資料,FragmentA 在onActivityResult()
中拿到資料
此時,有兩點需要特別注意:
-
不要使用
getActivity().startActivityForResult()
, 而是在Fragment中直接呼叫startActivityForResult()
-
activity 需要重寫 onActivityResult,其必須呼叫
super.onActivityResult(requestCode, resultCode, data)
以上兩點如果違反,則 onActivityResult 只能夠傳遞到 activity 的,無法傳遞到 Fragment
Result API
自1.3.0-alpha02
起,Fragment 支援 registerForActivityResult()
的使用,通過 Activity 的 ResultAPI 實現跨 Activity 通訊。
FragmentA 設定回撥:
```kotlin
class FragmentA : Fragment() {
private val startActivityLauncher: ActivityResultLauncher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivityLauncher.launch(Intent(requireContext(), ActivityB::class.java))
}
} ```
FragmentB 返回結果
kotlin
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
瞭解 Activity Result API 的同學對上述過程應該很熟悉。
簡單看一下原始碼。
原始碼基於 androidx.fragment:fragment:1.3.0
我們在 FragmentA 中通過建立一個 ActivityResultLauncher
,然後呼叫 launch 啟動目標 ActivityB
```java //Fragment # prepareCallInternal
return new ActivityResultLauncher() { @Override public void launch(I input, @Nullable ActivityOptionsCompat options) { ActivityResultLauncher delegate = ref.get(); if (delegate == null) { throw new IllegalStateException("Operation cannot be started before fragment " + "is in created state"); } delegate.launch(input, options); }
//...
};
```
可以看到,內部呼叫了delegate.launch
, 我們追溯一下 delegate 的出處,即 ref
中設定的 value
```java //Fragment # prepareCallInternal
registerOnPreAttachListener(new OnPreAttachedListener() { @Override void onPreAttached() { //ref中註冊了一個launcher,來自 registryProvider 提供的 ActivityResultRegistry final String key = generateActivityResultKey(); ActivityResultRegistry registry = registryProvider.apply(null); ref.set(registry.register(key, Fragment.this, contract, callback)); } });
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
@Override
public ActivityResultRegistry apply(Void input) {
//registryProvider 提供的 ActivityResultRegistry 來自 Activity
if (mHost instanceof ActivityResultRegistryOwner) {
return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
}
return requireActivity().getActivityResultRegistry();
}
}, callback);
}
```
上面可以看到 ref 中設定的 ActivityResultLauncher
來自 Activity 的 ActivityResultRegistry ,也就說 Fragment 的 launch,最終是由其 mHost 的 Activity 代理的。
後續也就是 Activity 的 Result API 的流程了,我們知道 Activity Result API 本質上是基於 startActivityForResult 實現的,具體可以參考這篇文章,本文不再贅述了
總結
本文總結了 Fragment 通訊的幾種常見方式,著重分析了 Result API
實現原理。
fragment-1.3.0
以後,對於一次性通訊推薦使用 Result API
替代舊有的 startActivityForResult
;響應式通訊場景則推薦使用 ViewModel + LiveData (or StateFlow)
, 儘量避免使用 EventBus
這類工具進行通訊。
- Google I/O :Android Jetpack 最新變化(二) Performance
- Google I/O :Android Jetpack 最新變化(一) Architecture
- Google I/O :Android Jetpack 最新變化(四)Compose
- Google I/O :Android Jetpack 最新變化(三)UI
- 一文看懂 Jetpack Compose 快照系統
- 聊聊 Kotlin 代理的“缺陷”與應對
- AAB 扶正!APK 再見!
- 面試必備:Kotlin 執行緒同步的 N 種方法
- Jetpack MVVM 七宗罪之六:ViewModel 介面暴露不合理
- CreationExtras 來了,建立 ViewModel 的新方式
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼
- 為什麼 RxJava 有 Single / Maybe 等單發資料型別,而 Flow 沒有?
- 使用整潔架構優化你的 Gradle Module
- 一道面試題:介紹一下 Fragment 間的通訊方式?
- 【程式碼吸貓】使用 Google MLKit 進行影象識別
- Kotlin 1.6 正式釋出,帶來哪些新特性?
- Android Dev Summit '21 精彩內容盤點
- @OnLifecycleEnvent 被廢棄,替代方案更簡單
- Jetpack Navigation 實現自定義 View 導航
- 實現一個 Coroutine 版 DialogFragment