This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.


There are many ways to implement the Android dialog box. Currently, DialogFragment is recommended. Use AlertDialog first to avoid the disappearance caused by configuration changes such as screen rotation. But its API is built on callbacks and is not friendly to use. Plug in the Coroutine and we can modify it.

1. Retrofit with Coroutine

User-defined AlertDialogFragment Inherits from DialogFragment as follows

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
    }
}
Copy the code

The implementation is simple: we use suspendCoroutine to convert the listener-based callback into a suspend function. Now we can get the return value of the dialog synchronously:

button.setOnClickListener {
    GlobalScope.launch {
        val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
        Log.d("AlertDialogFragment"."$result Clicked")}}Copy the code

2. Screen rotation crash

After testing, the above code was found to have problems. We know that DialogFragment can stay in place while the screen rotates, but if the Dialog button is clicked, it will crash:

kotlin.UninitializedPropertyAccessException: lateinit property _cont has not been initialized
Copy the code

If you understand how fragments and activities destroy and rebuild, you can easily deduce the cause of the problem:

  1. When you rotate the screen, the Activity will be recreated.
  2. Activity will be on his deathbedonSaveInstanceState()Kept inDialogFragmentThe state of theFragmentManagerState;
  3. After the Activity is rebuilt, theonCreate()According to thesavedInstanceStateforFragmentManagerStateAutomatic reconstructionDialogFragmentandshow()Come out

The process is summarized as follows:

Rotating screen – > Activity. OnSaveInstanceState () – > Activity. The onCreate () – > DialogFragment. The show ()

A reconstructed FragmentDialog whose member variable _cont has not been initialized will crash its access.

What if you don’t use LateInit? We tried to revamp it by introducing RxJava


3. Secondary transformation: RxJava + Coroutine

The Subject in RxJava avoids lateinit and prevents crashes:

//build.gradle
implementation "IO. Reactivex. Rxjava2: rxjava: 2.2.8"
Copy the code

The new AlertDialogFragment code is as follows:

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) }
    }
}
Copy the code

When a Dialog is displayed, respond to the listener’s callback by subscribing to SingleSubject.

After modification, the crash no longer occurred when the Dialog button was clicked after the screen was rotated, but there was still a problem: after the screen was rotated, we could not receive the return value of the Dialog, that is, the following log did not display as expected

Log.d("AlertDialogFragment", "$result Clicked")
Copy the code

When the DialogFragment is rebuilt, the Subject also follows the reconstruction, but the previous Subscriber is lost. Therefore, after clicking the button, the downstream of Rx cannot respond.

Is there any way to restore the previous Subscriber when the Subject is rebuilt? At this point, I thought of using onSaveInstanceState.

To save a Subject as Fragment arguments to savedInstanceState, it must be either Serializable or Parcelable,


4. Three times a makeover: SerializableSingleSubject

Happily, SingleSubject’s member variables are all subclasses of Serializable. If SingleSubject implements Serializable, you can store savedInstanceState. But unfortunately it is not, and it is a final class, had to copy the source code, you achieve a SerializableSingleSubject:

/** * Implement Serializable interface and add 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;

    // Omit the following code as SingleSubject

Copy the code

Based on the SerializableSingleSubject rewrite AlertDialogFragment is as follows:

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) }
    }
}
Copy the code

After the reconstruction, the previous Subscriber is restored through savedInstanceState. The downstream receives messages and logs are normally output.

Note that there are still hidden hazards: after the screen is rotated, clicking on the dialog will normally return the value, but the context of the coroutine recovery is the previous launch {… } closure

    GlobalScope.launch {
        val frag = AlertDialogFragment()
        val result = frag.showAsSuspendable(supportFragmentManager)
        Log.d("AlertDialogFragment"."$result Clicked on $frag")}Copy the code

If launch{… } references an external Activity (get a member), which is also an old Activity, and you need to be careful to avoid similar operations here.


5. Pure RxJava mode

Now that WE’ve introduced RxJava, here’s a final word on a version that relies on RxJava without Coroutine:

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

Subscribe () is used instead of a suspend function.

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

Please feel free to discuss in the comments section. The nuggets will draw 100 nuggets in the comments section after the diggnation project. See the event article for details