實現一個 Coroutine 版 DialogFragment
highlight: androidstudio theme: hydrogen
本文已參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金。
Android 對話方塊有多種實現方法,目前比較推薦的是 DialogFragment
,先比較與直接使用 AlertDialog
,可以避免螢幕旋轉等配置變化造成消失。但是其 API 建立在回撥的基礎上使用起來並不友好。接入 Coroutine 我們可以對其進行一番改造。
1. 使用 Coroutine 進行改造
自定義 AlertDialogFragment
繼承自 DialogFragment
如下
```kotlin class AlertDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
_cont.resume(which)
}
return AlertDialog.Builder(context)
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}
private lateinit var _cont : Continuation<Int>
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
_cont = cont
}
} ```
實現很簡單,我們是使用 suspendCoroutine
將原本基於 listener 的回撥轉化為掛起函式。接下來我們可以用同步的方式獲取 dialog 的返回值了:
kotlin
button.setOnClickListener {
GlobalScope.launch {
val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked")
}
}
2. 螢幕旋轉時的崩潰
經過測試,發現上述程式碼存在問題。我們知道 DialogFragment 在螢幕旋轉時可以保持不消失,但是此時如果點選 Dialog 的按鈕,會出現崩潰:
log
kotlin.UninitializedPropertyAccessException: lateinit property _cont has not been initialized
如果瞭解 Fragment 和 Activity 銷燬重建的過程就能輕鬆推理出發生問題的原因:
- 旋轉螢幕時,Activity 將會重新建立。
- Activity 臨終前會在
onSaveInstanceState()
中儲存DialogFragment
的狀態FragmentManagerState
; - 重建後的 Activity,在
onCreate()
中根據savedInstanceState
所給予的FragmentManagerState
自動重建DialogFragment
並且show()
出來
總結起來流程如下:
旋轉螢幕 --> Activity.onSaveInstanceState() --> Activity.onCreate() --> DialogFragment.show()
重建後的 FragmentDialog 其成員變數 _cont
尚未初始化,此時對其訪問自然發生 crash。
那麼如果不使用 lateinit
就沒問題了呢? 我們嘗試引入 RxJava 對其進行改造
3. 二次改造: RxJava + Coroutine
通過 RxJava 的 Subject
避免了 lateinit
的出現,防止 crash :
groovy
//build.gradle
implementation "io.reactivex.rxjava2:rxjava:2.2.8"
新的 AlertDialogFragment
程式碼如下:
```kotlin class AlertDialogFragment : DialogFragment() {
private val subject = SingleSubject.create<Int>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}
return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
} ``` 顯示 dialog 時,通過訂閱 SingleSubject 響應 listener 的回撥。
經過修改,旋轉屏幕後點選 Dialog 按鈕時沒有再發生 crash 的現象,但是仍然存在問題:螢幕旋轉後我們無法接收到 Dialog 的返回值,即沒有按預期的那樣顯示下面的日誌
Log.d("AlertDialogFragment", "$result Clicked")
當 DialogFragment 重建後, Subject 也跟隨重建,但是丟失了之前的 Subscriber
,所以點選按鈕後,Rx 的下游無法響應。
有沒有辦法讓 Subject 重建時能夠恢復之前的 Subscriber 呢? 此時想到了藉助 onSaveInstanceState
。
想要 subject 作為 Fragment 的 arguments
儲存到 savedInstanceState
,必須是一個 Serializable
或者 Parcelable
,
4. 三次改造: SerializableSingleSubject
令人高興的是,查閱 SingleSubject
原始碼後發現其成員變數全是 Serializable 的子類,也就是隻要 SingleSubject 實現 Serializable 介面就可以存入 savedInstanceState
了, 但可惜它不是,而且它是一個 final
類,只好拷貝原始碼出來,自己實現一個 SerializableSingleSubject :
```java
/*
* 實現 Serializable 介面並增加 serialVersionUID
/
public final class SerializableSingleSubject
final AtomicReference<SerializableSingleSubject.SingleDisposable<T>[]> observers;
@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] EMPTY = new SerializableSingleSubject.SingleDisposable[0];
@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] TERMINATED = new SerializableSingleSubject.SingleDisposable[0];
final AtomicBoolean once;
T value;
Throwable error;
// 以下程式碼同 SingleSubject,省略
```
基於 SerializableSingleSubject
重寫 AlertDialogFragment
如下:
```kotlin class AlertDialogFragment : DialogFragment() {
private var subject = SerializableSingleSubject.create<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
subject = it["subject"] as SerializableSingleSubject<Int>
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}
return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable("subject", subject);
}
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
} ```
重建後通過 savedInstanceState
恢復之前的 Subscriber
,下游順利收到訊息,日誌正常輸出。
需要注意的是,此時仍然存在隱患:螢幕旋轉後,點選 dialog 雖然可以正常獲得返回值,但是此時協程恢復的上下文是前一次 launch { ... }
的閉包
kotlin
GlobalScope.launch {
val frag = AlertDialogFragment()
val result = frag.showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked on $frag")
}
如上,此時列印的 frag 是重建之前的 DialogFragment,如果 launch{...}
裡引用了外部 Activity(獲取成員) ,那也是舊的 Activity,此處需要特別注意避免類似操作。
5. 純 RxJava 方式
既然引入了 RxJava,最後捎帶介紹一下不使用 Coroutine 只依靠 RxJava 的版本:
kotlin
fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
show(fm, tag)
return subject.hide()
}
使用時,由 subscribe()
替代掛起函式的使用。
kotlin
button.setOnClickListener {
AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
Log.d("AlertDialogFragment", "$result Clicked")
}
}
歡迎在評論區討論,掘金官方將在掘力星計劃活動結束後,在評論區抽送100份掘金周邊,抽獎詳情見活動文章
- 探索 Jetpack Compose 核心:深入 SlotTable 系統
- 盤點 Material Design 3 帶來的新變化
- Compose 動畫邊學邊做 - 夏日彩虹
- 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 精彩內容盤點