【Kotlin协程】当DialogFragment遇上Coroutine

【Kotlin协程】当DialogFragment遇上Coroutine_第1张图片
Android对话框有多种实现方法,目前比较推荐的是DialogFragment,相对于直接使用AlertDialog来说,可以避免屏幕旋转会的消失。但是其建立在回调基础上的API使用起来并不友好。好在有RxJava、Coroutine等优秀的工具,我们可以对其进行一番改造。

基于Coroutine+RxJava的改造


build.gradle

dependencies {
    // 省略

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$latest_version"
    implementation 'io.reactivex.rxjava2:rxjava:$latest_version'
    implementation 'io.reactivex.rxjava2:rxkotlin:$latest_version'
    implementation 'io.reactivex.rxjava2:rxandroid:$latest_version'

    // 省略
}

kotlin {
    experimental {
        coroutines "enable"
    }
}

继承DialogFragment

class AlertDialogFragment : DialogFragment() {

    private val subject = SingleSubject.create<Int>()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = { _: DialogInterface, which: Int ->
            subject.onSuccess(which)
        }

        return AlertDialog.Builder(activity)
                .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) }
    }
}

使用

button.setOnClickListener {
    launch(UI) {
        val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
        Log.d("AlertDialogFragment", "$result Clicked")
    }
}

屏幕旋转问题


当屏幕旋转时,会返现上述设置的Listener失效,只要理解了Fragment 和 Activity 生命周期就会知道问题的原因:

  1. 旋转屏幕时,Activity将会被重新创建。
  2. Activity临终前会在onSaveInstanceState()中保存 DialogFragment的状态FragmentManagerState;
  3. 重建后的Activity,在onCreate()中会根据savedInstanceState所给予的FragmentManagerState自动实例化DialogFragment,并且 show()出来

总结起来流程如下:

旋转屏幕-->-Activity.onSaveInstanceState()-->-Activity.onCreate()-->- DialogFragment.show()

协程的改造让DialogFragment结果变成同步读取,但其本质上是把suspend后面的代码在编译期变成了回调,可以理解为设置了一个Listener。那问题来了,由于横竖屏导致Fragment的重建,造成Listener丢失,此时点击按钮无法再出现预期log:

  Log.d("AlertDialogFragment", "$result Clicked")

对于这种情况,一般有两种解决办法:

  1. 重新设置Listener。特别是对于Listener包含对宿主Activity引用的情况(匿名内部类或者被Activity实现)情况下,由于Activity也重建导致闭包过期,需要更新Listener
  2. 将Listener通过argumentssavedInstanceState进行保存后恢复

我们通过实现一个可序列化的Subject实现第二种方法

SerializableSingleSubject

序列化的SingleSubject,可以将其Subscriber可以内部状态进行保存,并恢复使用

/**
 * implements 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;

    // 省略

AlertDialogFragment

基于SerialzableSingleSubject,重新实现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, which: Int ->
            subject.onSuccess(which)
        }

        return AlertDialog.Builder(activity)
                .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恢复之前的Subject/Subscriber,从而保证点击有效。


RxJava版本


当然,也可以脱离协程,仅使用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")
    }
}

你可能感兴趣的:(Kotlin,#,Kotlin,Coroutine)