Ein neuer Blick auf die Anzeige von Dialogen in Android


Das Bild zeigt den ersten Gedanken des Lesers, der sich fragt, was über eine so einfache Aufgabe wie das Anzeigen eines Dialogs geschrieben werden kann. Der Manager denkt ähnlich: "Hier gibt es nichts Kompliziertes, unser Vasya wird es in 5 Minuten tun." Natürlich übertreibe ich, aber in Wirklichkeit ist nicht alles so einfach, wie es auf den ersten Blick scheint. Besonders wenn wir über Android sprechen.


2019 war also auf dem Hof ​​und wir wissen immer noch nicht, wie man Dialoge richtig anzeigt .


Lassen Sie es uns der Reihe nach tun und mit der Erklärung des Problems beginnen:


Es ist erforderlich, einen einfachen Dialog mit dem Text anzuzeigen, um die Aktion und die Schaltflächen „Bestätigen / Abbrechen“ zu bestätigen. Durch Klicken auf die Schaltfläche "Bestätigen" - Führen Sie eine Aktion aus, klicken Sie auf die Schaltfläche "Abbrechen" - schließen Sie den Dialog.

Stirnlösung


Ich würde diese Methode als Junior bezeichnen, da dies nicht das erste Mal ist, dass ich auf ein Missverständnis stoße, warum Sie AlertDialog nicht einfach verwenden können, wie unten gezeigt:


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

Ein für Anfänger unerfahrener Weg, der offensichtlich und intuitiv ist. Aber wie in vielen Fällen bei der Arbeit mit Android ist diese Methode völlig falsch. Aus heiterem Himmel kommt es zu einem Speicherverlust. Schalten Sie einfach das Gerät aus, und in den Protokollen wird der folgende Fehler angezeigt:


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) 

Bei Stackoverflow ist die Frage zu diesem Thema eine der beliebtesten. Kurz gesagt, das Problem ist, dass wir entweder den Dialog anzeigen oder den Dialog nach Abschluss der Aktivierung nicht schließen.


Sie können natürlich im Dialogfeld in der Aktivität onPause oder onDestroy die Option "Entlassen" aufrufen, wie in der Antwort als Referenz angegeben . Aber genau das brauchen wir nicht. Wir möchten, dass der Dialog nach dem Drehen des Geräts wiederhergestellt wird.


Veralteter Weg


Bevor Fragmente in Android angezeigt wurden, sollten Dialoge durch einen Aufruf der Aktivierungsmethode showDialog angezeigt worden sein. In diesem Fall verwaltet die Aktivität den Lebenszyklus des Dialogs korrekt und stellt ihn nach einer Runde wieder her. Die Erstellung des Dialogs selbst musste im onCreateDialog-Rückruf implementiert werden:


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

Es ist nicht sehr praktisch, dass Sie eine Dialogkennung starten und Parameter über das Bundle übergeben müssen. Und wir können immer noch das Problem "Durchgesickertes Fenster" bekommen, wenn wir versuchen, einen Dialog anzuzeigen, nachdem wir onDestroy für die Aktivität aufgerufen haben. Dies ist beispielsweise möglich, wenn versucht wird, nach einer asynchronen Operation einen Fehler anzuzeigen.


Im Allgemeinen ist dieses Problem typisch für Android, wenn Sie nach einem asynchronen Vorgang etwas tun müssen und die Aktivität oder das Fragment zu diesem Zeitpunkt bereits zerstört ist. Dies ist wahrscheinlich der Grund, warum MV * -Muster in der Android-Community beliebter sind als bei iOS-Entwicklern.


Methode aus der Dokumentation


Fragmente wurden in Android Honeycomb angezeigt , und die oben beschriebene Methode ist veraltet, und die showDialog- Methode der Aktivität ist als veraltet markiert. Nein, AlertDialog ist nicht veraltet, da sich viele irren. Gerade gibt es DialogFragment , das das Dialogobjekt umschließt und seinen Lebenszyklus steuert.


Native Snippets sind seit der 28 API ebenfalls veraltet. Jetzt sollten Sie nur die Implementierung aus der Support Library (AndroidX) verwenden.

Lassen Sie uns unsere Aufgabe erfüllen, wie in der offiziellen Dokumentation vorgeschrieben :


  1. Zuerst müssen Sie von DialogFragment erben und die Erstellung eines Dialogs in der onCreateDialog-Methode implementieren.
  2. Beschreiben der Dialogereignisschnittstelle und Instanziieren des Listeners in der onAttach-Methode.
  3. Implementieren Sie eine Dialogereignisschnittstelle in einer Aktivität oder einem Fragment.

Wenn dem Leser nicht klar ist, warum der Hörer nicht durch den Konstruktor geleitet werden kann, kann er hier mehr darüber lesen

Dialogfragmentcode:


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

Aktivierungscode:


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

Es hat sich genug Code herausgestellt, oder?


In der Regel gibt es eine Art MVP im Projekt, aber ich habe entschieden, dass Presenter-Aufrufe in diesem Fall weggelassen werden können. Im obigen Beispiel lohnt es sich, die statische Methode zum Erstellen des newInstance-Dialogfelds und zum erwarteten Übergeben von Parametern an die Fragmentargumente hinzuzufügen.


Und das alles, damit sich der Dialog rechtzeitig versteckt und korrekt wiederhergestellt wird. Es ist nicht überraschend, dass solche Fragen bei Stackoverflow auftauchen: eins und zwei .


Die perfekte Lösung finden


Der aktuelle Stand der Dinge passte nicht zu uns, und wir suchten nach einer Möglichkeit, die Arbeit mit Dialogen komfortabler zu gestalten. Es gab das Gefühl, dass man es einfacher machen kann, fast wie bei der ersten Methode.


Die folgenden Überlegungen haben uns geleitet:


  • Muss ich den Dialog speichern und wiederherstellen, nachdem der Bewerbungsprozess abgebrochen wurde?
    In den meisten Fällen ist dies nicht erforderlich, wie in unserem Beispiel, wenn Sie eine einfache Nachricht anzeigen oder etwas fragen müssen. Ein solcher Dialog ist relevant, bis die Aufmerksamkeit des Benutzers verloren geht. Wenn Sie es nach einer langen Abwesenheit in der Anwendung wiederherstellen, verliert der Benutzer den Kontext mit der geplanten Aktion. Daher müssen Sie nur die Umdrehungen des Geräts unterstützen und den Lebenszyklus des Dialogs korrekt handhaben. Andernfalls kann der Benutzer aufgrund der unangenehmen Bewegung des Geräts die gerade geöffnete Nachricht verlieren, ohne sie zu lesen.
  • Bei Verwendung von DialogFragment wird zu viel Code angezeigt, und die Einfachheit geht verloren. Daher wäre es schön, das Fragment als Wrapper zu entfernen und Dialog direkt zu verwenden . Dazu müssen Sie den Status des Dialogfelds speichern, um ihn nach dem erneuten Erstellen der Ansicht wieder anzuzeigen und auszublenden, wenn die Ansicht stirbt.
  • Jeder ist es gewohnt, die Darstellung des Dialogs als Team wahrzunehmen, insbesondere wenn Sie nur mit MVP arbeiten. Die Aufgabe der anschließenden Wiederherstellung des Zustands übernimmt der FragmentManager. Sie können diese Situation jedoch anders betrachten und beginnen, den Dialog als Zustand wahrzunehmen . Dies ist viel praktischer, wenn Sie mit PM- oder MVVM-Mustern arbeiten.
  • Angesichts der Tatsache, dass die meisten Anwendungen jetzt reaktive Ansätze verwenden, müssen Dialoge reaktiv sein . Die Hauptaufgabe besteht nicht darin, die Kette zu unterbrechen, die die Anzeige des Dialogs initiiert, und einen reaktiven Strom von Ereignissen anzuhängen, um ein Ergebnis daraus zu erhalten. Dies ist auf der PresentationModel / ViewModel-Seite sehr praktisch, wenn Sie mehrere Datenströme bearbeiten.

Wir haben alle oben genannten Anforderungen berücksichtigt und eine Möglichkeit gefunden, Dialoge reaktiv anzuzeigen, die wir erfolgreich in unserer RxPM- Bibliothek implementiert haben (es gibt einen separaten Artikel darüber).


Die Lösung selbst erfordert keine Bibliothek und kann separat durchgeführt werden. Geleitet von der Idee des „Dialogs als Zustand“ können Sie versuchen, eine Lösung zu erstellen, die auf dem trendigen ViewModel und LiveData basiert. Aber ich werde dieses Recht dem Leser überlassen, und dann werden wir über eine fertige Lösung aus der Bibliothek sprechen.


Reaktive Methode


Ich werde zeigen, wie die anfängliche Aufgabe in RxPM gelöst wird, aber zuerst ein paar Worte zu Schlüsselkonzepten aus der Bibliothek:


  • PresentationModel - speichert einen Reaktionszustand, enthält UI-Logik und überlebt Runden.
  • Zustand ist ein reaktiver Zustand . Sie können sich das als Wrapper über BehaviorRelay vorstellen.
  • Aktion - Ein Wrapper über PublishRelay dient zum Übertragen von Ereignissen von View zu PresentationModel.
  • Staat und Aktion haben beobachtbare und Verbraucher.

Die DialogControl- Klasse ist für den Status des Dialogs verantwortlich. Es gibt zwei Parameter: den ersten für den Datentyp, der im Dialogfeld angezeigt werden soll, den zweiten für den Ergebnistyp. In unserem Beispiel ist der Datentyp Einheit, es kann sich jedoch um eine Nachricht an den Benutzer oder einen anderen Typ handeln.


DialogControl verfügt über die folgenden Methoden:


  • show(data: T) - gibt nur einen Befehl zum Anzeigen an.
  • showForResult(data: T): Maybe<R> - zeigt einen Dialog an und öffnet den Stream, um das Ergebnis zu erhalten.
  • sendResult(result: R) - sendet das Ergebnis und wird von der sendResult(result: R) aufgerufen.
  • dismiss() - versteckt nur den Dialog.

DialogControl speichert den Status - gibt es einen Dialog auf dem Bildschirm oder nicht (Angezeigt / Abwesend). So sieht es im Klassencode aus:


 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() } // ... } 

Erstellen Sie ein einfaches PresentationModel:


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

Bitte beachten Sie, dass die Klickverarbeitung, Bestätigungsbestätigung und Aktionsverarbeitung in derselben Kette implementiert sind. Auf diese Weise können Sie den Code fokussieren und die Logik nicht auf mehrere Rückrufe verteilen.


Als Nächstes binden wir DialogControl einfach mit der Erweiterung bindTo an die Ansicht.
Wir sammeln den üblichen AlertDialog und senden das Ergebnis über 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 } } 

In einem typischen Szenario passiert so etwas unter der Haube:


  1. Wir klicken auf die Schaltfläche, das Ereignis über die Aktion "buttonClicks" gelangt in das PresentationModel.
  2. Für dieses Ereignis starten wir die Anzeige des Dialogfelds über den Aufruf von showForResult.
  3. Infolgedessen ändert sich der Status in DialogControl von Abwesend zu Angezeigt.
  4. Wenn das angezeigte Ereignis empfangen wird, wird das Lambda aufgerufen, das wir in der bindTo-Bindung übergeben haben. Darin wird ein Dialogobjekt erstellt, das dann angezeigt wird.
  5. Der Benutzer drückt die Schaltfläche Bestätigen, der Listener wird ausgelöst und das Ergebnis des Klicks wird durch Aufrufen von sendResult an DialogControl gesendet.
  6. Als nächstes fällt das Ergebnis in das interne Aktionsergebnis, und der Status von Angezeigt ändert sich in Abwesend.
  7. Wenn ein Abwesenheitsereignis empfangen wird, wird der aktuelle Dialog geschlossen.
  8. Das Ereignis aus dem Aktionsergebnis fällt in den Stream, der durch den Aufruf von showForResult geöffnet wurde und von der Kette in PresentationModel verarbeitet wird.

Es ist zu beachten, dass der Dialog geschlossen wird, auch wenn die Ansicht von PresentationModel getrennt wird. In diesem Fall bleibt der Status angezeigt. Es wird bei der nächsten Bindung empfangen und der Dialog wird wiederhergestellt.


Wie Sie sehen können, ist DialogFragment nicht mehr erforderlich. Das Dialogfeld wird angezeigt, wenn die Ansicht an das PresentationModel angehängt ist, und wird ausgeblendet, wenn die Ansicht gelöst wird. Aufgrund der Tatsache, dass der Status in DialogControl gespeichert ist, das wiederum in PresentationModel gespeichert ist, wird der Dialog wiederhergestellt, nachdem das Gerät gedreht wurde.


Dialoge richtig schreiben


Wir haben verschiedene Möglichkeiten zur Anzeige von Dialogen untersucht. Wenn Sie immer noch auf die erste Weise zeigen, dann bitte ich Sie, tun Sie dies nicht mehr. Für Liebhaber von MVP bleibt nichts anderes übrig, als die Standardmethode zu verwenden, die in der offiziellen Dokumentation beschrieben ist. Leider lässt die Tendenz zur Imperativität dieses Musters nichts anderes zu. Nun, ich empfehle RxJava-Fans, sich die reaktive Methode und unsere RxPM- Bibliothek genauer anzusehen.

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


All Articles