[Kotlin Coroutine] When DialogFragment meets Coroutine

Insert picture description here
There are many ways to implement the Android dialog. The current recommendation is DialogFragmentto AlertDialogavoid the disappearance of screen rotation compared to direct use . But its API based on callback is not friendly to use. Fortunately, there are excellent tools such as RxJava, Coroutine, etc., we can make some changes to them.

Transformation based on 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"
    }
}

Inherit 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) }
    }
}

use

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

Screen rotation problem


When the screen rotates, it will return that the Listener set above is invalid. As long as you understand the life cycle of Fragment and Activity, you will know the cause of the problem:

  1. When the screen is rotated, the Activity will be recreated.
  2. onSaveInstanceState()The state of the DialogFragment will be saved in the Activity before the death FragmentManagerState;
  3. Activity reconstructed at onCreate()will according savedInstanceStateadministered FragmentManagerStateautomatically instantiates DialogFragment, and show()out

In summary, the process is as follows:

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

The transformation of the coroutine allows the DialogFragmentresult to be read synchronously, but in essence it turns the code behind the suspend into a callback during the compilation period, which can be understood as setting a Listener. The problem is coming. Due to the reconstruction of the Fragment caused by the horizontal and vertical screens, the Listener is lost. At this time, clicking the button can no longer display the expected log:

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

For this situation, there are generally two solutions:

  1. Reset the Listener. Especially for the case where the Listener contains a reference to the host Activity (anonymous inner class or implemented by the Activity), the closure expires because the Activity is also rebuilt, and the Listener needs to be updated
  2. Pass Listener argumentsor savedInstanceStaterestore after saving

We Subjectimplement the second method by implementing a serializable

SerializableSingleSubject

Serialized SingleSubject, you can save its Subscriberinternal state and restore it to use

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

Based on SerialzableSingleSubject, re-implement 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) }
    }
}

After rebuilding, the savedInstanceStateprevious Subject/ is restored Subscriberto ensure that the click is valid.


RxJava version


Of course, you can also leave the coroutine and only use RxJava

fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
    
    
    show(fm, tag)
    return subject.hide()
}

When used, subscribe()the call of the suspended function is replaced by

button.setOnClickListener {
    
    
    AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe {
    
     result ->
        Log.d("AlertDialogFragment", "$result Clicked")
    }
}

Guess you like

Origin blog.csdn.net/vitaviva/article/details/107744388