Architektonischer Aufbau eines Android Single Activity Screens
Die Entwicklung von modernen nativen Android-Apps basiert heutzutage tendenziell auf Single-Activity-Architekturen. Dies bedeutet, dass eine einzelne Activity als Parent über ein Fragment oder mehrere Fragmente als Children verfügen kann.
Innerhalb eines Events oder bei einer Navigation werden lediglich die Fragmente ersetzt, sodass die ursprüngliche Parent Activity weiterhin als Container fungiert. Zu beachten ist, dass ein Child Fragment hierarchisch gesehen selbst wiederum über Child Fragments verfügen kann.
Möglichkeiten zur Laufzeitkommunikation zwischen den Fragmenten
Durch diesen architektonischen Ansatz entstehen neue Herausforderungen wie beispielsweise die Kommunikation unter den Fragmenten zur Laufzeit. Hat man bei einem Aufruf eines neuen Fragments die Möglichkeit, Informationsparameter mit zu übergeben, stehen einem bei der Kommunikation unterhalb der Fragmente und gegebenenfalls der Parent Activity mehrere Möglichkeiten zur Verfügung:
- Interfaces
- SharedViewModel
- setTargetFragment()/onActivityResult()
Interfaces für die Kommunikation zwischen Fragmenten und Activities zu nutzen, ist ein denkbarer, allerdings auch ein veralteter Ansatz, der mit einem gewissen Overhead verbunden und in der Form aktuell nicht von Google direkt empfohlen wird. Ein SharedViewModel scheint auf den ersten Blick eine One-fits-all-Lösung darzustellen, die zudem von Google für die Kommunikation zwischen den Fragmenten empfohlen wird.
Hierbei teilen sich die Parent Activity und die Fragmente ein gemeinsames ViewModel. Ein beliebiges Startfragment könnte über das Observer Pattern mittels LiveData oder Flows entsprechende Events feuern, auf die dann in dem jeweiligen Zielfragment reagiert werden kann. Für die meisten Situationen ist dies ein gangbarer Weg, stößt allerdings bei einem wiederverwendbarem Fragment oft an seine Grenzen – wie beispielsweise bei einem Fragment mit einer Suchmaske mit Darstellung der Suchergebnisse. Sobald verschiedene Interaktionsfragmente auch nur minimale individuelle Kommunikationsanforderungen mit dem Suchfragment aufweisen, kann dies sehr schnell zu einem komplexen, fehleranfälligen sowie unübersichtlichen Overhead führen.
Die Kommunikation zur Laufzeit zwischen den Fragmenten mit einem Intent über die Target Fragment APIs (setTargetFragment/onActivityResult) in Verbindung mit einem requestCode und einem ResultCode sind seit API Level 28 und bei der fragment-ktx-Bibliothek ab Version 1.3.0-alpha04 veraltet. Stattdessen werden für die Kommunikation zwischen den Fragmenten die neuen Fragment Result APIs empfohlen.
Fragment Result API
Google beschreibt die Verwendung der Jetpack-Fragment-Bibliothek mit einem konsistenten Verhalten auf allen Geräten sowie einem Zugriff auf den Lifecycle. Seit der Version Fragment-ktx:1.3.0-alpha04 (29.04.2020) können FragmentManager für die Kommunikation unter Fragments verwendet werden. Seit dem 10.02.2021 ist die Version 1.3.0 als stabil veröffentlicht worden.
Dabei übernimmt der FragmentManager jetzt standardmäßig die Implementierung vom FragmentResultOwner und überschreibt die Methoden zum Setzen und Bereinigen von Fragment Results als auch Fragment Result Listener.
Die Funktionsweise kann folgendermaßen beschrieben werden:
- Der FragmentManager hält die Fragment Results und die Fragment Result Listener in unterschiedlichen Maps mit einem requestKey als einzigartige ID.
- Der Fragment Result Listener, der mit einem entsprechenden requestKey registriert wurde, wird benachrichtigt, sobald die Results gesetzt oder aktualisiert werden.
- Ein „resultCode“ wie bei der Nutzung von onActivityResult wird nicht mehr benötigt, zudem werden Daten über ein Bundle gesendet und empfangen. Hierbei können alle Datentypen verwendet werden, die ein Bundle unterstützt.
- Auf diese Weise können Fragments im selben FragmentManager kommunizieren sowie von Child- zu Parent-Fragmenten oder umgekehrt.
Kommunikation über denselben FragmentManager
Eine Kommunikation zwischen zwei Fragmenten auf demselben FragmentManager findet immer dann statt, wenn die Fragmente auf derselben Hierarchieebene kommunizieren. Dies kann folgendermaßen veranschaulicht werden:
Der Sender FragmentA sendet über den Befehl setFragmentResult() den Namen eines Freundes an den FragmentManager. Die Nachricht wird über einen Request Key und der Name des Freundes über einen Bundle Key verpackt.
Das Empfängerfragment FragmentB empfängt, sobald es den Zustand „STARTED“ erreicht hat, über den Aufruf von setFragmentResultListener () das Ergebnis und führt den Listener Callback für den Request Key aus, über den zuvor vom Sender eine Nachricht versendet wurde. Über die entsprechenden Bundle Keys erhält man die Werte der Nachricht.
In diesem Beispiel wurde ein Wert übergeben sowie unmittelbar danach empfangen und anschließend einem LiveData-Attribut in dem ViewModel des Zielfragments zugeordnet. Sowohl Sender als auch Empfänger greifen hierbei auf denselben FragmentManager zu.
Kommunikation zwischen Parent- und Child-Fragmenten
Wie in der Einführung bereits erwähnt, können Fragmente hierarchisch gesehen selbst wiederum Fragmente beherbergen. In diesem Fall benötigt das Parent Fragment den childFragmentManager, um Daten vom Child Fragment zu empfangen, oder selbst wiederum Daten zum Child Fragment zu senden.
Wie im vorherigen Beispiel erhält das Zielfragment die Daten, sobald es den Zustand „STARTED“ erreicht hat.
Das Child Fragment setzt beziehungsweise ruft seine Nachricht über seinen FragmentManager ab.
Daten in der Parent Activity erhalten
Wenn die Parent Activity Nachrichten von einem Fragment erhalten möchte, wird in diesem Fall der supportFragmentManager benötigt.
Was ist zu beachten?
- Es kann nur ein Listener zu einem konkreten Request Key registriert werden
- Falls mehr als ein Listener auf dem gleichen Key registriert wird, wird der vorherige durch den neuesten Listener ersetzt.
- Wenn mehrfach setFragmentResult() vom Senderfragment gefeuert wird, erhält das Zielfragment immer den aktuellsten Wert.
- Wenn ein Wert über setFragmentResult() gesendet wird und kein Listener auf den Key registriert ist, wird der aktuellste Wert im FragmentManager gespeichert, bis ein Listener mit dem gleichen Key angemeldet wird. Dieses Verhalten entspricht einem „hot Observable“.
- Sobald der Listener angemeldet und der Fragment Lifecycle den Status „Lifecycle.State.STARTED“ erreicht hat, wird der Wert unmittelbar danach konsumiert.
- Der Wert wird vom FragmentManager gelöscht, sobald das Zielfragment es konsumiert hat.
Fazit
Mit der neuen Fragment Result API ist eine Kommunikation von einfachen und kleinen Datenpaketen zwischen Fragmenten zur Laufzeit noch einfacher und zudem wurde überflüssiger Overhead entfernt. Unter Berücksichtigung des Lifecycles und der Prinzipien zum Konsumverhalten der Nachrichten wie beispielsweise Einzigartigkeit und Aktualität einer Nachricht ist zudem eine gewisse Stabilität garantiert, sodass die Fragment Result API für die Laufzeitkommunikation untereinander eine sehr gute Alternative oder zumindest eine Ergänzung zu einem SharedViewModel darstellt.
Zudem löst es sehr effizient das Problem zwischen mehrfach wiederverwendeten Fragmenten, die in bestimmten Views einen unterschiedlichen Austausch von Daten benötigen.