Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment
,先比较与直接使用 AlertDialog
,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine 我们可以对其进行一番改造。
1. 使用 Coroutine 进行改造
自定义 AlertDialogFragment
继承自 DialogFragment
如下
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 的返回值了:
button.setOnClickListener { GlobalScope.launch { val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager) Log.d("AlertDialogFragment", "$result Clicked") } }
2. 屏幕旋转时的崩溃
经过测试,发现上述代码存在问题。我们知道 DialogFragment 在屏幕旋转时可以保持不消失,但是此时如果点击 Dialog 的按钮,会出现崩溃:
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 :
//build.gradle implementation "io.reactivex.rxjava2:rxjava:2.2.8"
新的 AlertDialogFragment
代码如下:
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 :
/** * 实现 Serializable 接口并增加 serialVersionUID */ public final class SerializableSingleSubject<T> extends Single<T> implements SingleObserver<T>, Serializable { private static final long serialVersionUID = 1L; 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
如下:
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 { ... }
的闭包
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 的版本:
fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> { show(fm, tag) return subject.hide() }
使用时,由 subscribe()
替代挂起函数的使用。
button.setOnClickListener { AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result -> Log.d("AlertDialogFragment", "$result Clicked") } }