There are many ways to implement the Android dialog. The current recommendation is DialogFragment
to AlertDialog
avoid 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:
- When the screen is rotated, the Activity will be recreated.
onSaveInstanceState()
The state of the DialogFragment will be saved in the Activity before the deathFragmentManagerState
;- Activity reconstructed at
onCreate()
will accordingsavedInstanceState
administeredFragmentManagerState
automatically instantiates DialogFragment, andshow()
out
In summary, the process is as follows:
旋转屏幕-->-Activity.onSaveInstanceState()-->-Activity.onCreate()-->- DialogFragment.show()
The transformation of the coroutine allows the DialogFragment
result 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:
- 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
- Pass Listener
arguments
orsavedInstanceState
restore after saving
We Subject
implement the second method by implementing a serializable
SerializableSingleSubject
Serialized SingleSubject
, you can save its Subscriber
internal 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 savedInstanceState
previous Subject
/ is restored Subscriber
to 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")
}
}