实现一个 Coroutine 版 DialogFragment

简介: 实现一个 Coroutine 版 DialogFragment

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 销毁重建的过程就能轻松推理出发生问题的原因:

  1. 旋转屏幕时,Activity 将会重新创建。
  2. Activity 临终前会在 onSaveInstanceState() 中保存 DialogFragment 的状态 FragmentManagerState;
  3. 重建后的 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")
    }
}
目录
相关文章
|
5月前
|
前端开发 JavaScript
【HTML+CSS+JavaScript】animated-countdown
【HTML+CSS+JavaScript】animated-countdown
30 0
|
Go 调度 C#
Unity——协程(Coroutine)
Unity——协程(Coroutine)
352 0
|
Java
Imageloader<5>-ImageLoader的变量初始化
Imageloader<5>-ImageLoader的变量初始化
62 0
|
安全 Java 调度
Looper 需要手动 quit,那主线程 Looper 呢?
Looper 需要手动 quit,那主线程 Looper 呢?
|
Android开发 Java 数据格式
DialogFragment源码分析
目录介绍 1.最简单的使用方法 1.1 官方建议 1.2 最简单的使用方法 1.3 DialogFragment做屏幕适配 2.源码分析 2.1 DialogFragment继承Fragment 2.
1188 0
IntentService详解
IntentService是什么? 这篇文章是之前就写好的,一直没有整理出来,这几天有空正好整理发布一下。 我们知道Service可以让我们在后台处理一些事情,但是Service实际上也是主线程,所以执行长耗时任务时依然会ANR,只不过ANR触发时间要比前台长。一般我们会在Service中开启一个子线程去完成耗时任务。 而IntentService就是解决这个问题的,它是Service的一个抽象子类,需要实现onHandleIntent,代码在这个函数中执行。它与Service最大的不同就是默认开启一个子线程,而onHandleIntent就是在子线程中执行的。
156 0
|
XML Android开发 数据格式
startActivityForResult()和onSaveInstanceState()用法
该方法作用是:A Activity跳转到B Activity携带返回数据 MainActivity的xml布局内代码: MainActivity,class文件内部代码 package com.
1129 0