Un nouveau regard sur l'affichage des dialogues dans Android


L'image montre la première pensée du lecteur qui se demande ce qui peut être écrit sur une tâche aussi simple que l'affichage d'un dialogue. Le manager pense de la même manière: "Il n'y a rien de compliqué ici, notre Vasya fera en 5 minutes." Bien sûr, j'exagère, mais en réalité tout n'est pas aussi simple qu'il y paraît à première vue. Surtout si nous parlons d'Android.


Donc, 2019 était dans la cour et nous ne savons toujours pas comment afficher correctement les dialogues .


Faisons-le dans l'ordre et commençons par l'énoncé du problème:


Il est nécessaire d'afficher un dialogue simple avec le texte pour confirmer l'action et les boutons «confirmer / annuler». En cliquant sur le bouton «confirmer» - effectuer une action, par le bouton «annuler» - fermer la boîte de dialogue.

Solution de front


J'appellerais cette méthode junior, car ce n'est pas la première fois que je rencontre un malentendu pourquoi vous ne pouvez pas simplement utiliser AlertDialog, comme indiqué ci-dessous:


AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { dialog, which -> // handle click } .setNegativeButton("Cancel", null) .create() .show() 

Un moyen assez courant pour un développeur novice, il est évident et intuitif. Mais, comme dans de nombreux cas lorsque vous travaillez avec Android, cette méthode est complètement fausse. À l'improviste, nous obtenons une fuite de mémoire, il suffit de tourner l'appareil, et vous verrez l'erreur suivante dans les journaux:


stacktrace
 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) 

Sur Stackoverflow, la question à ce sujet est l'une des plus populaires. En bref, le problème est que nous affichons la boîte de dialogue ou ne fermons pas la boîte de dialogue une fois l'activation terminée.


Vous pouvez, bien sûr, appeler le renvoi sur la boîte de dialogue dans l'activité onPause ou onDestroy, comme indiqué dans la réponse par référence . Mais ce n'est pas exactement ce dont nous avons besoin. Nous voulons que le dialogue reprenne après avoir tourné l'appareil.


Manière obsolète


Avant que des fragments n'apparaissent dans Android, les dialogues auraient dû être affichés via un appel à la méthode d' activation showDialog . Dans ce cas, l'activité gère correctement le cycle de vie du dialogue et le restaure après un tour. La création du dialogue lui-même devait être implémentée dans le rappel onCreateDialog:


 public class MainActivity extends Activity { private static final int CONFIRMATION_DIALOG_ID = 1; // ... @Override protected Dialog onCreateDialog(int id, Bundle args) { if (id == CONFIRMATION_DIALOG_ID) { return new AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // handle click } }) .create(); } else { return super.onCreateDialog(id, args); } } } 

Ce n'est pas très pratique que vous ayez à démarrer un identifiant de dialogue et à passer des paramètres via le Bundle. Et nous pouvons toujours obtenir le problème de «fenêtre perdue» si nous essayons d'afficher une boîte de dialogue après avoir appelé onDestroy sur l'activité. Cela est possible, par exemple, lorsque vous essayez d'afficher une erreur après une opération asynchrone.


En général, ce problème est typique pour Android, lorsque vous devez faire quelque chose après une opération asynchrone et que l'activité ou le fragment est déjà détruit à ce moment. C'est probablement pourquoi les modèles MV * sont plus populaires dans la communauté Android que parmi les développeurs iOS.


Méthode issue de la documentation


Des fragments sont apparus dans Android Honeycomb , et la méthode décrite ci-dessus est déconseillée, et la méthode showDialog de l' activité est marquée comme déconseillée. Non, AlertDialog n'est pas obsolète, car beaucoup se trompent. Tout à l'heure, il y a DialogFragment , qui enveloppe l'objet de dialogue et contrôle son cycle de vie.


Les extraits natifs sont également obsolètes depuis l'API 28. Maintenant, vous devez utiliser uniquement l'implémentation de la bibliothèque de support (AndroidX).

Accomplissons notre tâche, comme prescrit par la documentation officielle :


  1. Vous devez d'abord hériter de DialogFragment et implémenter la création d'une boîte de dialogue dans la méthode onCreateDialog.
  2. Décrivez l'interface des événements de dialogue et instanciez l'écouteur dans la méthode onAttach.
  3. Implémentez une interface d'événement de dialogue dans une activité ou un fragment.

Si le lecteur ne sait pas très bien pourquoi l'auditeur ne peut pas passer par le constructeur, il peut en savoir plus ici

Code de fragment de dialogue:


 class ConfirmationDialogFragment : DialogFragment() { interface ConfirmationListener { fun confirmButtonClicked() fun cancelButtonClicked() } private lateinit var listener: ConfirmationListener override fun onAttach(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 exception throw ClassCastException(activity.toString() + " must implement ConfirmationListener") } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialog.Builder(context!!) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { _, _ -> listener.confirmButtonClicked() } .setNegativeButton("Cancel") { _, _ -> listener.cancelButtonClicked() } .create() } } 

Code d'activation:


 class MainActivity : AppCompatActivity(), ConfirmationListener { private fun showConfirmationDialog() { ConfirmationDialogFragment() .show(supportFragmentManager, "ConfirmationDialogFragmentTag") } override fun confirmButtonClicked() { // handle click } override fun cancelButtonClicked() { // handle click } } 

Assez de code s'est avéré, non?


En règle générale, il existe une sorte de MVP dans le projet, mais j'ai décidé que les appels des présentateurs peuvent être omis dans ce cas. Dans l'exemple ci-dessus, cela vaut la peine d'ajouter la méthode statique de création de la boîte de dialogue newInstance et de passer des paramètres aux arguments de fragment, le tout comme prévu.


Et tout cela pour que le dialogue se cache dans le temps et se rétablisse correctement. Il n'est pas surprenant que de telles questions se posent sur Stackoverflow: un et deux .


Trouver la solution parfaite


La situation actuelle ne nous convenait pas et nous avons commencé à chercher un moyen de rendre le travail avec les dialogues plus confortable. Il y avait un sentiment que vous pouvez le rendre plus facile, presque comme dans la première méthode.


Voici les considérations qui nous ont guidés:


  • Dois-je enregistrer et restaurer la boîte de dialogue après avoir tué le processus de candidature?
    Dans la plupart des cas, cela n'est pas obligatoire, comme dans notre exemple, lorsque vous devez afficher un message simple ou demander quelque chose. Un tel dialogue est pertinent jusqu'à ce que l'attention de l'utilisateur soit perdue. Si vous le restaurez après une longue absence dans l'application, l'utilisateur perdra le contexte de l'action planifiée. Par conséquent, il vous suffit de prendre en charge les tours de l'appareil et de gérer correctement le cycle de vie du dialogue. Sinon, à cause du mouvement maladroit de l'appareil, l'utilisateur peut perdre le message qui vient d'être ouvert sans le lire.
  • Lorsque vous utilisez DialogFragment, trop de code passe-partout apparaît, la simplicité est perdue. Par conséquent, ce serait bien de se débarrasser du fragment comme wrapper et d' utiliser Dialog directement . Pour ce faire, vous devrez enregistrer l'état de la boîte de dialogue afin de l'afficher à nouveau après avoir recréé la vue et la masquer lorsque la vue s'éteint.
  • Tout le monde a l'habitude de percevoir l'affichage du dialogue en équipe, surtout si vous ne travaillez qu'avec MVP. La tâche de la restauration ultérieure de l'état est assumée par le FragmentManager. Mais vous pouvez regarder cette situation différemment et commencer à percevoir le dialogue comme un état . C'est beaucoup plus pratique lorsque vous travaillez avec des modèles PM ou MVVM.
  • Étant donné que la plupart des applications utilisent désormais des approches réactives, les dialogues doivent être réactifs . La tâche principale n'est pas de briser la chaîne qui déclenche l'affichage du dialogue, et d'attacher un flux réactif d'événements pour en obtenir un résultat. C'est très pratique du côté PresentationModel / ViewModel lorsque vous manipulez plusieurs flux de données.

Nous avons pris en compte toutes les exigences ci-dessus et avons trouvé un moyen d'afficher de manière réactive les dialogues, que nous avons mis en œuvre avec succès dans notre bibliothèque RxPM (il y a un article séparé à ce sujet).


La solution elle-même ne nécessite pas de bibliothèque et peut être effectuée séparément. Guidé par l'idée de «dialogue en tant qu'état», vous pouvez essayer de construire une solution basée sur le ViewModel et LiveData à la mode. Mais je laisserai ce droit au lecteur, puis nous parlerons d'une solution toute faite de la bibliothèque.


Méthode réactive


Je vais montrer comment la tâche initiale est résolue dans RxPM, mais d'abord quelques mots sur les concepts clés de la bibliothèque:


  • PresentationModel - stocke un état de réaction, contient une logique d'interface utilisateur, survit aux virages.
  • L'État est un état réactif. Vous pouvez le considérer comme un wrapper sur BehaviorRelay.
  • Action - un wrapper sur PublishRelay, sert à transférer des événements de View vers PresentationModel.
  • État et action ont observable et consommateur.

La classe DialogControl est responsable de l'état de la boîte de dialogue. Il a deux paramètres: le premier pour le type de données à afficher dans la boîte de dialogue, le second pour le type de résultat. Dans notre exemple, le type de données sera Unit, mais il peut s'agir d'un message à l'utilisateur ou de tout autre type.


DialogControl a les méthodes suivantes:


  • show(data: T) - donne juste une commande à afficher.
  • showForResult(data: T): Maybe<R> - affiche une boîte de dialogue et ouvre le flux pour obtenir le résultat.
  • sendResult(result: R) - envoie le résultat, est appelé du côté Affichage.
  • dismiss() - masque simplement la boîte de dialogue.

DialogControl stocke l'état - y a-t-il un dialogue à l'écran ou non (affiché / absent). Voici à quoi cela ressemble dans le code de classe:


 class DialogControl<T, R> internal constructor(pm: PresentationModel) { val displayed = pm.State<Display>(Absent) private val result = pm.Action<R>() sealed class Display { data class Displayed<T>(val data: T) : Display() object Absent : Display() } // ... } 

Créez un modèle de présentation simple:


 class SamplePresentationModel : PresentationModel() { enum class ConfirmationDialogResult { CONFIRMED, CANCELED } //        enum    val confirmationDialog = dialogControl<Unit, ConfirmationDialogResult>() val buttonClicks = Action<Unit>() override fun onCreate() { super.onCreate() buttonClicks.observable .switchMapMaybe { //           confirmationDialog.showForResult(Unit) .filter { it == ConfirmationDialogResult.CONFIRMED } } .subscribe { //   } .untilDestroy() } } 

Veuillez noter que le traitement des clics, la confirmation de la confirmation et le traitement des actions sont implémentés dans la même chaîne. Cela vous permet de concentrer le code et de ne pas disperser la logique sur plusieurs rappels.


Ensuite, nous lions simplement DialogControl à la vue à l'aide de l'extension bindTo.
Nous collectons le AlertDialog habituel et envoyons le résultat via sendResult:


 class SampleActivity : PmSupportActivity<SamplePresentationModel>() { override fun providePresentationModel() = SamplePresentationModel() //     View  PresentationModel override fun onBindPresentationModel(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 } } 

Dans un scénario typique, quelque chose comme ça se passe sous le capot:


  1. Nous cliquons sur le bouton, l'événement à travers l'action "buttonClicks" entre dans le PresentationModel.
  2. Pour cet événement, nous lançons l'affichage de la boîte de dialogue via l'appel à showForResult.
  3. Par conséquent, l'état dans DialogControl passe d'Absent à Displayed.
  4. Lorsque l'événement affiché est reçu, le lambda que nous avons transmis dans la liaison bindTo est appelé. Un objet de dialogue y est créé, qui s'affiche ensuite.
  5. L'utilisateur appuie sur le bouton Confirmer, l'écouteur se déclenche et le résultat du clic est envoyé à DialogControl en appelant sendResult.
  6. Ensuite, le résultat tombe dans le «résultat» de l'action interne et l'état de Affiché passe à Absent.
  7. Lorsqu'un événement absent est reçu, la boîte de dialogue actuelle se ferme.
  8. L'événement du «résultat» de l'action tombe dans le flux qui a été ouvert par l'appel à showForResult et est traité par la chaîne dans PresentationModel.

Il convient de noter que la boîte de dialogue se ferme même lorsque View est détaché de PresentationModel. Dans ce cas, le statut reste affiché. Il sera reçu lors de la prochaine reliure et le dialogue sera rétabli.


Comme vous pouvez le voir, le besoin de DialogFragment a disparu. La boîte de dialogue s'affiche lorsque la vue est attachée au modèle de présentation et est masquée lorsque la vue est déliée. En raison du fait que l'état est stocké dans DialogControl, qui à son tour est stocké dans PresentationModel, la boîte de dialogue est restaurée après la rotation de l'appareil.


Ecrire correctement les dialogues


Nous avons examiné plusieurs façons d'afficher les boîtes de dialogue. Si vous montrez toujours de la première manière, alors je vous en prie, ne le faites plus. Pour les amateurs de MVP, il ne reste plus qu'à utiliser la méthode standard, qui est décrite dans la documentation officielle. Malheureusement, la tendance à l'impérativité de ce schéma ne permet pas de faire autrement. Eh bien, je recommande aux fans de RxJava de regarder de plus près la méthode réactive et notre bibliothèque RxPM .

Source: https://habr.com/ru/post/fr440284/


All Articles