Komponentenbasierte Entwicklung und Wiederverwendung eines Shopping Carts
Im letzten Teil unserer „Jetpack Compose“-Reihe haben wir uns mit der Entwicklung und Implementierung eines Shopping Carts beschäftigt.
Auf Grundlage der zuvor erarbeitenden Ergebnisse für die technische Umsetzung können die geschnittenen Komponenten nun sukzessive mithilfe von Jetpack Compose umgesetzt werden.
Implementierung: Initialisierung, Zustände und Events
Die Interaktionen zwischen dem Nutzer und dem User Interface werden über Composable-Funktionen und einem ViewModel reguliert. Das ViewModel dient für die Composable-Funktionen auf Bildschirmebene als Source-of-Truth für den UI-State. Sie sorgt auch für den Zugang zur Geschäftslogik der Domänen- und Datenschicht.
Um die zuvor erwähnte Wiederverwendung sowie die einfache Nutzung von Preview-Composable-Funktionen zu ermöglichen, werden für die Initialisierungen Composable-Funktionen verwendet. An dem Eintrittspunkt handelt es sich um eine stateful und für das explizite User Interface eine stateless Funktion.
Dabei kommunizieren das ViewModel und das User Interface über Events und States. Auf User Interface-Ebene werden die Zustände bezüglich folgender Entitäten benötigt und auf Änderungen beobachtet:
- Liste der Shopping Cart-Artikel
- der aktuelle Gesamtpreis aller Waren im Shopping Cart
- Zustand des CTA-Buttons
Als Events, die von den Composable-Funktionen an das ViewModel gesendet werden, werden zum einen das Entfernen eines Artikels und zum anderen das Betätigen des Call-to-Action-Buttons benötigt.
Zu beachten ist, dass das Entfernen eines Artikels aus dem Shopping Cart eine Listenelement-Operation ist und somit zunächst über die jeweiligen ItemViewModel
verarbeitet wird. Um eine konsistente Wiederverwendung der Listenelement-Operationen im Kontext des Shopping Carts in einem anderen Use Case zu gewährleisten, wird eine abstrakte ParentItemViewModel-Klasse erzeugt. Beispielsweise bei einer Reklamation kann das Logging dafür zentralisiert werden.
Für die Nutzung von Zuständen bieten sich vor allem Enum- oder Sealed-Klassen an, welche den Gebrauch über eine State Variable ermöglichen. Diese sind bei Bedarf beliebig skalierbar.
Aufteilung in Komponenten
Die zuvor in Komponenten geschnittenen Bereiche werden in einzelne Composable-Funktionen aufgeteilt. Danach erfolgt eine Zusammenführung auf High-Level-Ebene. Die einzelnen Composable-Komponenten können so lose gekoppelt voneinander entwickelt und getestet werden.
Header
Für die Entwicklung des Headers werden lediglich statische Elemente dargestellt. In der ersten Version benötigen diese keine Interaktionen. Somit müssen auch keine Parameter in der Signatur definiert werden. Allerdings werden hier bereits das
Padding, die User-Icon-Größe sowie zwei konkrete Ausprägungen der Text-Composable und der User-Avatar über das Designsystem wiederverwendet.
In der Regel gibt es eine übersichtliche Anzahl von wiederverwendbaren Dimensionen und UI-Elementen. Diese Komponenten lassen sich über die gesamte Anwendung konsistent wiederverwenden.
Entsprechend simpel ist die Umsetzung der Preview Funktion:
Shopping Cart-Liste
Bei der Shopping Cart-Liste kann das Compose Framework seine vollen Stärken ausspielen. Mit vergleichbar wenigen Zeilen ist die Darstellung der Liste inklusive der Interaktionen innerhalb der Listenelemente implementiert. Zudem kann die Listenelement-Komponente für optisch ähnliche Use Cases, wie z. B. einer Reklamation, wiederverwendet werden.
Innerhalb der CartListItem-Composable-Komponente wird der wiederverwendbare Teil implementiert, der von den verschiedenen Cart Use Cases verwendet werden kann.
Mithilfe des LazyItemScope ist die Verwendung des fillParentMaxWidth() Operators in Zeile 30 möglich. Das ist hilfreich, um die gesamte Breite eines Listenelements abzüglich der Padding-Abstände nutzen zu können.
Der Inhalt eines Listenelements ist eine Composable-Funktion, die innerhalb einer weiteren Composable-
Funktion gekapselt ist. Letztere ist für den grauen und abgerundeten Hintergrund zuständig und stellt einen BoxWithConstraintsScope zur Verfügung.
BoxWithConstraints ist ein Layout, das sich analog zum Box Layout verhält. Allerdings ermöglicht es die Nutzung der minimalen oder maximal verfügbare Breite und Höhe für die konkrete Composable-Funktion. In diesem Fall wird es für die maximal verfügbare Höhe des Artikel-Icons innerhalb der ShoppingCartItemBackground Composable Funktion genutzt.
Das ShoppingCartItemViewModel ist für die Operationen auf Listenelement-Ebene zuständig. Es feuert bei Bedarf ein Event, sobald ein Artikel aus dem Shopping Cart entfernt werden soll.
Innerhalb des ShoppingCartViewModel kann der EventState aus dem ItemViewmodel mithilfe eines snapshotFlows beobachtet werden. SnapshotFlow erzeugt einen Flow aus einem Observable Snapshot State (z. B. State Holder, die von einem mutableState zurückgegeben werden).
Für die Preview-Composable-Funktion ist die Angabe einer beliebig großen Liste möglich:
Shopping Cart-Bottom
In der Bottom Composable werden der dynamische Gesamtpreis alle Waren im Shopping Cart angezeigt sowie der Zustand des CTA-Buttons und ein onClick-Event, um den CashOut-Prozess anzustoßen.
Durch die Wiederverwendung von Composable-Funktionen aus dem Designsystem wird die Composable-Funktion für den Shopping Cart Bottom sehr schlank und übersichtlich gehalten.
Die ShoppingCartItemList ist vom Typ SnapshotStateList, weil variable Collections nicht innerhalb veränderlicher State Holder gelegt werden sollten. Siehe folgenden Beitrag für eine ausführliche Erläuterung: https://dev.to/zachklipp/two-mutables-dont-make-a-right-2kgp .
Die jeweiligen ShoppingCartItems werden im ShoppingCartViewModel von zwei State-Variablen zur Berechnung des Gesamtpreises und zur Bestimmung des CTA-Button-Zustands beobachtet. In der stateful-Composable-Funktion werden diese Ergebnisse selbst wiederum geprüft. Das Ergebnis wird an die ShoppingCartBottom Composable delegiert.
In der Preview Composable-Funktion kann z. B. der CTA-Button Zustand getestet werden – ganz ohne Notwendigkeit eines erneuten Builds.
Preview Composable-Funktionen
Als Ergebnis können wir alle Komponenten einzeln oder auch orchestriert darstellen:
Fazit
Im Vergleich zu der herkömmlichen Herangehensweise über einen Recyclerview-Adapter und diversen XML-Layouts lässt sich mit Jetpack Compose sowohl die Komplexität als auch die Lines-of-Code spürbar reduzieren.
Wendet man als User zusätzlich das Wissen aus den ersten beiden Beiträgen unserer Jetpack-Compose-Blogserie an, lässt sich die Komplexität des Shopping Carts positiv beeinflussen. Durch die Aufteilung in die jeweiligen Komponenten werden mögliche Anti-Pattern präventiv vermieden. Das ViewModel dient als Source-of-Truth für den UI-State und nutzt dem Zugang zur Geschäftslogik der Domänen- und Datenschicht.
Die feingranulare Aufteilung von einfachen UI-Elementen bis hin zu komplexeren UI-Komponenten ermöglicht eine intuitive Wiederverwendung bestimmter Bausteine. Das gilt auch für ähnliche Use Cases. Durch diesen modularen Ansatz ist eine einfache Erweiterbarkeit und Anpassbarkeit gewährleistet.
Gerade in der Retail-Branche, in der viel mit Listen und Listenelementen gearbeitet wird, kann Jetpack Compose seine Stärken ausspielen. Bei einem gut durchdachten Einsatz steigert das Android-Toolkit die Produktivität und reduziert die Fehleranfälligkeit.
Im Logcat sehen wir die entsprechenden Logausgaben, die wir in der Parent Methode removeArticleItemFromShoppingCart() definiert haben, sobald wir einen Artikel aus dem Shopping Cart entfernen.
Ausblick
In dem nächsten Jetpack Compose-Blogbeitrag stehen die modulare Architektur sowie die Navigation innerhalb dieser Architektur im Mittelpunkt:
- Ein effektiver Navigationslösungsansatz über die Navigation Compose Component in einem modularen Jetpack Compose-Projekt innerhalb und zwischen den Feature-Modulen
- Welche Probleme und Konflikte entstehen mit dem Standardansatz?ƒ
- Welcher Lösungsansatz ermöglicht eine reibungslose Navigation?
- Verwendung von Dependency Injection im Kontext von Jetpack Compose und der Navigation Compose Component