Jetpack Compose – Best Practices und Anti-Pattern

  • Frederick Klyk - adesso mobile
    Teamleiter App-Entwicklung, Software Architekt und Senior Software Engineer

Konzepte, Prinzipien und Aufbau einer Architektur in einem Multi-Module-Projekt

Im ersten Teil dieser Compose-Blogserie haben wir diverse Gründe und die sich daraus ergebenden Vorteile herausgearbeitet, die für den Einsatz von Compose sprechen und damit ein erstes gemeinsames Verständnis für die Arbeit mit Jetpack Compose geschaffen. Mit der Erörterung des Lifecycles sowie des Konzepts des „State-Hoistings“ wurden die elementaren Basics in Jetpack Compose auf High Level-Ebene betrachtet. Ferner wurden aus architektonischer und konzeptioneller Sicht ein erster architektonischer Entwurf für ein Multimodule-Projekt sowie ein typsicher architektonischer Aufbau eines Feature-Moduls auf Blackbox-Ebene untersucht.

Dieses erarbeitete theoretische Wissen dient nun im zweiten Teil dieser Jetpack Compose-Blogserie als Grundlage für die Erläuterung von Best Practices und der Umsetzung im praktischen Einsatz. Zudem werden Risiken, typische Anti-Pattern und Lösungsvorschläge als Gegenmaßnahmen bei dem Einsatz von Jetpack beleuchtet.

Best Practice bei der Nutzung von Composable-Funktionen

Bei der Nutzung von View-basierten XML-Layouts hat man die Möglichkeit, den geschriebenen XML-Code unmittelbar danach in einer Preview-Maske zu begutachten und bei Bedarf über „Drag and Drop“ anzupassen, ohne den Code vorher kompilieren zu müssen.

Compose bietet für alle Composable-Funktionen die Möglichkeit, sogenannte Preview-Funktionen zu nutzen, in der das visuelle Ergebnis einer Composable-Funktion für eine Vorschau abgebildet wird. Änderungen an der grafischen Struktur benötigen allerdings eine Vorkompilierung, wobei „weiche“ Änderungen wie zum Beispiel das Ändern des Darstellungstextes ohne weitere Kompilierungen direkt in der Vorschau angezeigt werden. Wie zuvor erwähnt, wird für den Zugriff komplexer Logik üblicherweise ein ViewModel verwendet. Bei der Verwendung der Jetpack Compose Navigation Component wird das ViewModel aufgrund des Lifecycles innerhalb der Navigation Component in den Composable Scope des jeweiligen Screens erstellt. Ein häufig verwendetes Anti-Pattern, welches oft auch in Einstiegtutorials beschrieben wird, ist die Composable-Funktion auf Bildschirmebene stateful aufzurufen.

Jetpack Compose Best Practices, Composable-Funktionen

Hierdurch wäre zum einen die Composable-Funktion nur bedingt wiederverwendbar und zum anderen würde die Realisierung einer Preview-Funktion durch die erforderliche Parameterübergabe eines ViewModels erschwert beziehungsweise bei vielen komplexen Abhängigkeiten nur sehr umständlich umsetzbar.

Jetpack Compose Best Practices, stateless Composable-Funktion

Ein genereller Best Practice-Ansatz ist es, die aufzurufende Composable-Funktion auf Bildschirmebene stateless zu halten. Dies wird ebenfalls von Google-Mitarbeitenden im Kontext von Compose mit folgenden Aussagen empfohlen (siehe https://jetc.dev/slack/2021-04-17-preview-viewmodel.html):

  • Best practice is probably to avoid referencing AAC ViewModels in your composable functions. They are not platform-agnostic and almost always imply a strong coupling between your composablefunction and the rest of your application.” – Jim Sproch, Google
  • “(…) read from the view model up at the top of your application and split it off into only the non-ViewModeldata that your individual composablesneed.” – Jim Sproch, Google
  • “When your composable does not behave well in Preview, that is almost always an indication your composable is not sufficiently isolated from the rest of the platform/application code.” – Jim Sproch, Google
  • „Another alternative is to pass a data class with all of your state and callback functions in it.” – Sean McQuillan, Google

In die Praxis übertragen bedeutet das, einer Composable-Funktion auf Bildschirmebene als Parameter nur das zu übergeben, was sie wirklich benötigt. Ein pragmatischer Musteransatz ist es, eine stateful und eine stateless Composable-Funktion von einem Screen zu erstellen. Dies ermöglicht eine problemlose und einfache Nutzung der Preview Composables für die relevanten UI-Komponenten.

stateful und stateless Composable-Funktion

Während die stateful Composable-Funktion das ViewModel erhält und dort die Zustände verwaltet, werden der stateless Composable-Funktion als Parameter lediglich die Werte der Zustände sowie die konkreten Ereignisse als Lambdafunktionen, die sie benötigt, übergeben.

Bei der Übergabe der Parameter zu der stateless Composable-Funktion hat sich folgende Best Practice-Reihenfolge bewährt:

  • Obligatorische Parameter, zum Beispiel Werte der Zustände und Ereignisse als Lambdafunktionen,
  • optionale Parameter mit Default-Werten, gegebenenfalls auch null,
  • Composable Content als letzten Parameter, optional mit einem definierten Scope, um die Nutzung von nachgestellten Lambdas zu ermöglichen.
Jetpack Compose Best Practices, Übergabe der Parameter zu der stateless Composable-Funktion

Dies ermöglicht sowohl problemlos und effektiv Preview Composable-Funktionen zu nutzen als auch die Composable-Funktionen selbst mithilfe von Mocks testen zu können.

Preview Composable-Funktionen

Zusammengefasst liegen die Vorteile durch die kombinierte Nutzung von stateful und stateless Composable Funktionen auf Bildschirmebene besonders in folgenden Punkten:

  • die Stateless Composable-Funktion „FeatureSecondContent“ ist auch für andere Use Cases wiederverwendbar
  • die Preview Composable für die Nutzung der Vorschau kann problemlos verwendet werden
  • das Testen der Composable-Funktionen wird durch die Entkopplung des Zustands von der Benutzeroberfläche, die ihn anzeigt und dem Mocken der Parameter vereinfacht
  • das vorgestellte Konzept entspricht dem Single Source of Truth-Prinzip.

Risiken, Anti-Pattern und Lösungsmaßnahmen

Bei der Verwendung von Jetpack Compose können sich durch das neue Programmparadigma ungewollt einige Anti-Pattern einschleichen, die zu einer Degeneration der ursprünglich aufgesetzten Architektur führen und schlimmstenfalls teure Refaktorisierungen zur Folge haben können.

In den nachfolgend aufgeführten Anti-Pattern werden neben den Risiken und Ursachen auch präventive Gegenmaßnahmen vorgestellt:

Gott-Klassen

Das Ziel von Fragmenten und Activities war es, sie möglichst schlank zu halten und stets nach dem Prinzip „So wenig wie möglich, so viel wie nötig“ zu handeln. Zudem wurden onClickListener und sonstige Aufruflogiken als auch die Darstellung von LiveData Variablen mittels Databinding in den XML-basierten Views verarbeitet. Weiterhin haben BindingAdapter bei der Unterstützung der View-Darstellung einiges an Boilerplate-Code separiert.

Wenn man die eben genannten Punkte jetzt zum größten Teil versucht, in Composable-Funktionen zu kompensieren, können mit fortlaufender Projektzeit sehr schnell Gott-Klassen entstehen. Selbstverständlich sollte man bei dem Einsatz von Jetpack Compose das Qualitätsmerkmal der Wartbarkeit weiterhin hoch priorisieren und schon frühzeitig und präventiv Gegenmaßnahmen für entstehende Gott-Klassen entwickeln. Ebenfalls können im Design System verschiedene Kategorisierungen durchgeführt werden, die je nach Belieben feingranular durchgeführt werden können. Dies kann beispielsweise die klare Trennung der fachlichen Verantwortlichkeiten in den jeweiligen Domänen sein. Eine Unterstützung zum Schneiden von unterschiedlichen fachlichen Verantwortlichkeiten kann die Festlegung von maximalen Zeilen pro Klasse im Linter sein.

„Callback Hell“ Composable-Funktionen

Ähnlich wie Gott-Klassen sind „Callback Hell“ Composable-Funktionen ein weiteres bekanntes Anti-Pattern, in das man schnell geraten kann. Dabei spielt der Name „Callback Hell“ auf die hierarchisch verschachtelten Callbacks in JavaScript ab und beschreibt in Compose die pyramidenmäßige Darstellung des Codes durch Verschachtelungen. Auch hier empfiehlt es sich, die Composable-Funktionen feingranular in Komponenten zu schneiden, sodass ein bestimmter Screen über mehrere unterschiedlichen Bausteine zusammengesetzt wird. Hierbei empfiehlt es sich, redundante View-Elemente im Design-System zu hinterlegen. Ebenfalls hilft hier die Festlegung von maximalen Zeilen pro Funktion.

Redundante und nicht preview-fähige Composable-Funktionen

Ein weiteres ungewolltes Anti-Pattern ist die redundante Erstellung von gleichen Composable View-Elementen in unterschiedlichen Screens durch stateful Composable-Funktionen. Gerade Einstiegstutorials für Jetpack Compose präsentieren häufig die Darstellung und Nutzung von stateful Composable-Funktionen, indem sie beispielsweise ein ViewModel oder Navigation (NavHost) Abhängigkeiten als Übergabeparameter in der Signatur zeigen. Wie erläutert, wird das Anti-Pattern deutlich, wenn Preview-Funktionen erschwert oder gar nicht realisierbar sind und die stateful Composable-Funktionen in anderen Screens nicht wieder verwendet werden können. Durch die kombinierte Nutzung von stateful und stateless Composable-Funktionen kann das Problem gelöst und das einfache Mocken in Preview-Funktionen als auch die Wiederverwendung der Composable-Funktionen ermöglicht werden. Es wird empfohlen, die bereits erwähnte Reihenfolge der Übergabeparameter in den stateless Composbale-Funktionen zu verwenden.

Rekomposition beachten

Im ersten Teil dieser Blogserie haben wir bei der Erläuterung des Lifecycle in Jetpack Compose darauf hingewiesen, das Composable-Funktionen bei Änderung ihrer Struktur beliebig häufig eine Rekomposition auslösen können. Dies kann unter Umständen zu spürbaren Performanz- und Batterieverlusten durch lange und komplexe Berechnungen führen, wenn falsch platzierte Aufgaben innerhalb der Neuzusammensetzung wiederholt aufgerufen werden.

Der sinnvolle Einsatz von „key“ Composables kann Compose dabei helfen, Composable-Instanzen in der Komposition zu identifizieren, sodass sie von der Rekomposition nicht betroffen sind und somit nicht neu zusammengesetzt werden.

Mischung der Single Source of Truth und State Holder-Quellen

Composable-Funktionen erlauben eine höhere Kohäsion, um beispielsweise eine geringere Kopplung zu erreichen. Es wird allerdings empfohlen, das ViewModel in der Regel konsistent als Single Source of Truth und als State Holder zu wählen. Dies bedeutet, dass so viel Logik wie möglich im ViewModel implementiert und die Nutzung von Logik in den Composable-Funktionen auf ein nötiges Minimum zu reduzieren. Weiterhin sollte der architektonische Aufbau innerhalb eines Feature-Moduls (in Teil 1 dieser Blogserie erläutert) eingehalten werden und eine Composable-Funktion beispielsweise nicht direkt mit dem Repository kommunizieren oder gar selbst Logik für einen Datenbank-Aufruf beinhalten, sondern dies stets über das ViewModel initiieren.

Eine Mischung kann schnell zu chaotischen Verhältnissen führen, und die Line, welche Komponenten für welche Art von Logik zuständig ist, wird zunehmend auf Kosten der Wartbarkeit und Erweiterbarkeit verwässert.

Fazit

Nach dem im ersten Teil die grundlegenden Prinzipien und Regeln von Jetpack Compose erörtert und der architektonische Aufbau innerhalb eines Feature Moduls auf High Level-Abstraktionsniveau beschrieben wurde, wurden in diesem Teil der Blogserie sowohl die Best Practices als auch die möglichen Anti-Pattern beim praktischen Einsatz von Jetpack Compose erläutert.

Neben der kombinierten Nutzung von stateful und stateless Composable-Funktionen ist das überlegte Schneiden von Composable-Funktionen von immenser Bedeutung. Hierbei bietet sich das Design System-Modul sehr gut an, um eine hohe Wiederverwendung in den jeweiligen Feature-Modulen zu gewährleisten. Typische Anti-Pattern, in die man bei dem Einsatz von Jetpack Compose in bei einem fortschreitenden Lebenszyklus eines Projekts schnell geraten kann, haben häufig lange und kostspielige Refaktorisierungsphasen zur Folge. Die genannten präventiven Gegenmaßnahmen sollten daher bereits sehr früh in einem Projekt berücksichtigt und hoch priorisiert werden.

Ausblick

Im dritten Teil dieser Blogserie werden die praktischen Vorteile von Jetpack Compose an einem realen Beispiel demonstriert. In dem Showcase wird die Implementierung eines handelsüblichen Shopping carts, das typischerweise ein Kernbestandteil in zahllosen Retail-Apps darstellt, mithilfe von Jetpack Compose beschrieben. Dabei werden das theoretische Wissen aus dem ersten Teil und die in diesem Artikel vermittelten Best Practices in der Praxis miteinander kombiniert.

Jetpack Compose

Konzepte, Prinzipien und Aufbau einer Architektur in einem Multi-Module-Projekt – Teil 1
Android Jetpack Compose

Lesen Sie auch...

×
Telefon

Sie sind auf der Suche nach einem Experten im Bereich App-Entwicklung? Wir freuen uns auf Ihre Nachricht!

+49 231 99953850
×