A fresh look at the display of dialogs in Android
In the picture, the first thought of the reader, who is perplexed, is that you can write about such a simple task as displaying a dialogue. The manager thinks the same way: “There is nothing complicated here, our Vasya will do it in 5 minutes.” Of course, I exaggerate, but in reality everything is not as simple as it seems at first glance. Especially if we are talking about Android.
So, it was the year 2019 outside, and we still do not know how to properly show the dialogues .
Let's do everything in order, and start by setting the task:
It is required to show a simple dialogue with the text to confirm the action and the "confirm / cancel" buttons. By pressing the “confirm” button - to perform an action, by the “cancel” button - to close the dialog.
The solution "in the forehead"
I would call this method Junior, because it is not the first time that I encounter a lack of understanding why it is impossible to simply use AlertDialog, as shown below:
AlertDialog.Builder(this)
.setMessage("Please, confirm the action")
.setPositiveButton("Confirm") { dialog, which ->
// handle click
}
.setNegativeButton("Cancel", null)
.create()
.show()
A fairly common way for a novice developer, it is obvious and intuitive. But, as in many cases when working with Android, this method is completely wrong. Out of the blue we get a memory leak, just turn the device, and you will see the following error in the logs:
E/WindowManager: android.view.WindowLeaked: Activity com.example.testdialog.MainActivity has leaked window DecorView@71b5789[MainActivity] that was originally added here
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:511)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
at android.app.Dialog.show(Dialog.java:329)
at com.example.testdialog.MainActivity.onCreate(MainActivity.kt:27)
at android.app.Activity.performCreate(Activity.java:7144)
at android.app.Activity.performCreate(Activity.java:7135)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2931)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6718)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
On Stackoverflow, the issue on this issue is one of the most popular. In short, the problem is that we either show the dialogue or do not close the dialogue after the completion of the work of activation.
You can, of course, call dismiss on the dialog in onPause or onDestroy activations, as advised in the response to the link . But this is not exactly what we need. We want the dialog to be restored after turning the device.
Outdated way
Before the appearance of fragments in Android, dialogs should have been displayed via a call to the showDialog activation method . In this case, activating correctly manages the life cycle of the dialogue and restores it after the turn. Creating the dialog itself needed to be implemented in the onCreateDialog callback:
publicclassMainActivityextendsActivity{
privatestaticfinalint CONFIRMATION_DIALOG_ID = 1;
// ...@Overrideprotected Dialog onCreateDialog(int id, Bundle args){
if (id == CONFIRMATION_DIALOG_ID) {
returnnew AlertDialog.Builder(this)
.setMessage("Please, confirm the action")
.setPositiveButton("Confirm", new DialogInterface.OnClickListener() {
@OverridepublicvoidonClick(DialogInterface dialog, int which){
// handle click
}
})
.create();
} else {
returnsuper.onCreateDialog(id, args);
}
}
}
It is not very convenient that you have to enter a dialog identifier and pass parameters through the Bundle. And we can still get the “leaked window” problem if we try to display a dialog after calling onDestroy on the activation. This is possible, for example, when trying to show an error after an asynchronous operation.
In general, this problem is typical for Android, when you need to do something after an asynchronous operation, and the activation or fragment has already been destroyed at this moment. That's probably why MV * patterns are more popular in the Android community than among iOS developers.
Method from documentation
In Android Honeycomb , fragments appeared, and the method described above is outdated, and the showDialog method of activation is marked as deprecated. No, AlertDialog is not outdated, as many are mistaken. Just now a DialogFragment appeared , which wraps the object of the dialogue and controls its life cycle.
Native fragments are also outdated since API 28. Now you should use only the implementation from the Support Library (AndroidX).
Let's carry out our task, as prescribed by the official documentation :
- First you need to inherit from DialogFragment and implement the creation of a dialog in the onCreateDialog method.
- Describe the dialogue event interface and instantiate the listener in the onAttach method.
- Implement the dialogue event interface in an activit or snippet.
If the reader is not very clear why the listener cannot be passed through the constructor, then he can read more about it here.
Dialogue snippet code:
classConfirmationDialogFragment : DialogFragment() {
interfaceConfirmationListener{
funconfirmButtonClicked()funcancelButtonClicked()
}
privatelateinitvar listener: ConfirmationListener
overridefunonAttach(context: Context?) {
super.onAttach(context)
try {
// Instantiate the ConfirmationListener so we can send events to the host
listener = activity as ConfirmationListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exceptionthrow ClassCastException(activity.toString() + " must implement ConfirmationListener")
}
}
overridefunonCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(context!!)
.setMessage("Please, confirm the action")
.setPositiveButton("Confirm") { _, _ ->
listener.confirmButtonClicked()
}
.setNegativeButton("Cancel") { _, _ ->
listener.cancelButtonClicked()
}
.create()
}
}
Activation code:
classMainActivity : AppCompatActivity(), ConfirmationListener {
privatefunshowConfirmationDialog() {
ConfirmationDialogFragment()
.show(supportFragmentManager, "ConfirmationDialogFragmentTag")
}
overridefunconfirmButtonClicked() {
// handle click
}
overridefuncancelButtonClicked() {
// handle click
}
}
A lot of code came out, right?
As a rule, there is some MVP in the project, but I decided that the presenter’s calls can be omitted in this case. In the example above, it is worthwhile to add a static method for creating the newInstance dialog and passing parameters to the fragment arguments, everything is as it should be.
And this is all for the sake of the dialogue hiding in time and properly restored. Not surprisingly, there are such questions on Stackoverflow: one and two .
Finding the perfect solution
The current state of affairs did not suit us, and we began to look for a way to make working with dialogues more comfortable. There was a feeling that you can make it easier, almost like in the first method.
The following are the considerations that guided us:
- Do I need to save and restore the dialogue after killing the application process?
In most cases, this is not required, as in our example, when you need to show a simple message or ask something. Such a dialogue is relevant until the user's attention is lost. If it is restored after a long absence in the application, the user will lose the context with the planned action. Therefore, it is only necessary to support the turns of the device and correctly handle the life cycle of the dialogue. Otherwise, from the awkward movement of the device, the user may lose the message just opened without reading it. - When using DialogFragment, too much boilerplate code appears, simplicity is lost. Therefore, it would be nice to get rid of the fragment as wrappers and use Dialog directly . To do this, you have to store the state of the dialog in order to show it again after the re-creation of the View and hide it when the View dies.
- Everyone is used to taking the dialogue show as a team, especially if you work only with MVP. The task of the subsequent restoration of the state takes on FragmentManager. But you can look at this situation differently and begin to perceive the dialogue as a state . This is much more convenient when working with PM or MVVM patterns.
- Given that most applications now use reactive approaches, there is a need for dialogs to be reactive . The main task is not to break the chain that initiates the display of the dialogue, and bind the reactive flow of events to get a result from it. This is very convenient on the PresentationModel / ViewModel side when you manipulate multiple data streams.
We took into account all the above requirements and came up with a method of reactive display of dialogs, which we successfully implemented in our RxPM library (there is a separate article about it ).
The solution itself does not require a library and can be made separately. Guided by the idea of “dialogue as a state”, you can try to build a solution based on trendy ViewModel and LiveData. But I will leave this right to the reader, and then we will speak about the ready-made solution from the library.
Reactive mode
I will show how the original problem is solved in RxPM, but first a few words about the key concepts from the library:
- PresentationModel - stores reactive state, contains UI-logic, is going through turns.
- State - jet state . Can be taken as a wrapper over BehaviorRelay.
- Action - a wrapper over PublishRelay, used to transfer events from View to PresentationModel.
- State and Action have observable and consumer.
For the state of the dialog class is responsible DialogControl . It has two parameters: the first for the type of data that should be displayed in the dialog, the second for the type of result. In our example, the data type will be Unit, but this can be a message to the user or any other type.
DialogControl has the following methods:
show(data: T)
- just gives the command to display.showForResult(data: T): Maybe<R>
- shows the dialogue and opens the stream to get the result.sendResult(result: R)
- sends result, is called from View.dismiss()
- just hides the dialogue.
DialogControl stores the state - is there a dialog on the screen or not (Displayed / Absent). This is how it looks in the class code:
classDialogControl<T, R> internalconstructor(pm: PresentationModel) {
val displayed = pm.State<Display>(Absent)
privateval result = pm.Action<R>()
sealedclassDisplay{
dataclassDisplayed<T>(valdata: T) : Display()
object Absent : Display()
}
// ...
}
Create a simple PresentationModel:
classSamplePresentationModel : PresentationModel() {
enumclassConfirmationDialogResult{
CONFIRMED, CANCELED
}
// Создаем контрол диалога без данных и с enum для возвращаемого результатаval confirmationDialog = dialogControl<Unit, ConfirmationDialogResult>()
val buttonClicks = Action<Unit>()
overridefunonCreate() {
super.onCreate()
buttonClicks.observable
.switchMapMaybe {
// по клику на кнопку запускаем диалог и ожидаем нужный результат
confirmationDialog.showForResult(Unit)
.filter { it == ConfirmationDialogResult.CONFIRMED }
}
.subscribe {
// обрабатываем действие
}
.untilDestroy()
}
}
Please note that the processing of clicks, receiving confirmation and processing actions are implemented in the same chain. This allows you to make the code focused and not spread logic across several callbacks.
Then simply bind DialogControl to View using extension bindTo.
We collect the usual AlertDialog, and send the result via sendResult:
classSampleActivity : PmSupportActivity<SamplePresentationModel>() {
overridefunprovidePresentationModel() = SamplePresentationModel()
// В этом методе связываем View и PresentationModeloverridefunonBindPresentationModel(pm: SamplePresentationModel) {
pm.confirmationDialog bindTo { data, dialogControl ->
AlertDialog.Builder(this@SampleActivity)
.setMessage("Please, confirm the action")
.setPositiveButton("Confirm") { _, _ ->
dialogControl.sendResult(CONFIRMED)
}
.setNegativeButton("Cancel") { _, _ ->
dialogControl.sendResult(CANCELED)
}
.create()
}
button.clicks() bindTo pm.buttonClicks
}
}
In a typical scenario, under the hood, something like this happens:
- We click on the button, the event through the Action "buttonClicks" enters the PresentationModel.
- For this event, we start the display of the dialogue by calling showForResult.
- As a result, the state in DialogControl changes from Absent to Displayed.
- When receiving a Displayed event, the lambda that we passed in the bindTo binding is called. It creates a dialog object, which is then displayed.
- The user clicks the “Confirm” button, the listener is triggered and the result of the click is sent to DialogControl by calling sendResult.
- Next, the result falls into the internal Action “result”, and the state from Displayed changes to Absent.
- When an Absent event is received, the current dialog is closed.
- The event from the Action “result” falls into the stream that was opened by showForResult and processed by the chain in PresentationModel.
It should be noted that the dialogue closes at the moment when the View is decoupled from the PresentationModel. In this case, the state remains Displayed. It will be received at the next binding and the dialog will be restored.
As you can see, the need for a DialogFragment is gone. The dialog is shown when the View is attached to the PresentationModel and is hidden when the View is unbound. Due to the fact that the state is stored in DialogControl, which in turn is stored in PresentationModel, the dialog is restored after turning the device.
Write conversations correctly
We have examined several ways to display dialogs. If you still show the first way, then I ask you, do not do more like this. For fans of MVP, nothing remains, how to use the standard method, which is described in the official documentation. Unfortunately, the tendency to imperativeness of this pattern does not allow to do otherwise. Well, I recommend RxJava fans to take a closer look at the reactive method and our RxPM library .