Navigation mit Android Jetpack Compose

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

Effektive und flexible Navigation mit Android Jetpack Compose in einem multimodularen Projekt

Einführung für Entwickler und Entwicklerinnen

Die Navigation zwischen verschiedenen Screens ist ein stetig wiederkehrendes Kernelement einer App. In der Regel wurde in der Android Entwicklung ein Single-Activity Ansatz gewählt, in denen in bestimmten Workflows beliebige Fragmente hinzugefügt bzw. ersetzt wurden. Hierbei konnte man sich von einer Vielzahl von Lösungsansätzen, wie unter anderem Intents, den Fragment Manager oder in der in der jüngeren Zeit erscheinenden Jetpack Navigation Component (https://developer.android.com/guide/navigation/navigation-getting-started) bedienen. 

In Android Jetpack Compose werden für die Darstellung des User Interfaces Composable-Funktionen anstelle von Fragmenten genutzt, sodass eine Navigation zwischen Composable Funktionen stattfindet. 

Mit der Veröffentlichung von Navigation Compose ist eine Unterstützung der Navigation Component für Android Jetpack Compose möglich. 

„The Navigation component provides support for Jetpack Compose applications. You can navigate between composables while taking advantage of the Navigation component’s infrastructure and features.” (Google) 

Mit dem UI-Framework Android Jetpack Compose in Verbindung mit Navigation Compose entstehen beim Layout der Navigation neue Herausforderungen, die mit dem in diesem Blogbeitrag beschriebenen Lösungsansatz effektiv gemeistert werden können. 

Multimodularer Aufbau in einem Android Jetpack Compose Projekt

n modernen mobilen Apps ist es zweckmäßig, ab einer bestimmten Projektgröße von einem Monolithen zu einem modularen Architekturansatz überzugehen. In dem hier schlicht gehaltenen fiktiven Compose Projekt werden zunächst N Featuremodule angelegt, die jeweils eine Abhängigkeit auf das Designsystem halten. Im Designsystem sollen eigene wiederverwendbare Compose-UI-Elemente hinterlegt werden, wie z.B. Navigation Drawer, Top-App-Bar oder Buttons. 

Navigation mit Android Jetpack Compose | Featuremodule

Neben einer Vielzahl von Vorteilen, die die Einführung eines modularen Architekturansatz mit sich bringt, korreliert die steigende Komplexität in der Entwicklung mit der Komplexität der Navigation zwischen den Modulen, weil man sich um die Navigation zwischen Composable-Funktionen innerhalb eines Modules sowie die Navigation zwischen den Modulen kümmern muss. 

Um diese Komplexität beherrschen zu können, ist eine nähere Betrachtung möglicher Navigationsansätze sowie der Navigation Compose Bibliothek angemessen. 

Jetpack Compose Navigation: Problematik mit dem Standardansatz

Bei der Betrachtung des Standardlösungsansatzes auf der Google Seite von Navigation Compose sowie auf verschiedenen weiteren Webseiten fällt auf, dass entweder alle Navigationspfade innerhalb des NavHost Blocks definiert werden oder dass jede Composable-Funktion eine Referenz auf den NavHostController besitzt. Daraus leiten sich im Code für beide Lösungsansätze folgende Probleme ab: 

Navigation mit Android Jetpack Compose | NavHostController

Ansatz 1: Alle Navigationspfade eines Moduls innerhalb des NavHost/NavGraphBuilder:

  • Alle Navigationspfade und möglichen Navigationswege werden in einer Composable-Funktion gekapselt, was schnell zu Unübersichtlichkeit führen kann 
  • Wenn von einer Composable zur nächsten Composable und von dort zur nächsten Composable usw. navigiert werden soll, müsste eine immer größer anwachsende Liste von Navigationsargumenten übergeben und bei jeder Composable-Funktion durchgereicht werden. Als ersten Ansatz könnte man eine Liste von Navigationspfaden übergeben, was sich allerdings zurecht nicht sauber anfühlt. 
  • Die Auflösung der hartkodierten Strings in allen Modulen nutzbaren Variablen ist noch nicht geklärt 
  • Zudem müsste stets der NavHostController in die jeweiligen FeatureModule als Referenz für die Navigation übergeben werden, was zu deutlich wachsendem Boilerplatecode führt. 

Ansatz 2: Jede navigierbare Composable-Funktion erhält eine NavHostController Referenz

  • Der Ansatz, dass jede Composable Funktion, die zu einer anderen Composable Funktion navigieren soll, eine Referenz des NavHostControllers benötigt, skaliert nicht gut und erfordert das Durchreichen dieser Referenz 
  • Die Navigationslogik wird mit der UI-Logik in den Composable-Funktionen vermengt, was das isolierte Testen der Navigationslogik erschwert 
  • Bei größer anfallenden Projekten verliert man schnell die Übersicht der Nachvollziehbarkeit der Navigationsstrecken, wenn Navigationspfade dezentral über die verschiedenen Composable Funktionen verstreut sind, was sowohl die Wartung als auch die Modifizierbarkeit erschweren 

Ansatz 3: Implementierung einer flexiblen Navigation über das Navigation Modul und ViewModel

  • Trennung von UI-Quellcode in Composable Funktionen und Anwendungslogik durch Ausgliedern der Navigation-Logik in den jeweiligen ViewModels 
  • Zentralisierte Deklaration der Navigationsstrecken in den jeweiligen Feature-Modulen vereinfacht die Nachvollziehbarkeit und Änderbarkeit 
  • Keine durchschleifen der NavHostController-Referenz in den jeweiligen UI-Composable Funktionen für die Navigation zwischen Composable-Funktionen und Feature-Modulen 
  • Definition des Navigationsgraphen an einer zentralen Stelle mit Darstellung aller Subgraphen der jeweiligen Feature-Module 
  • Gut skalierbarer Ansatz bei der Verwendung weiterer Feature-Module 
Navigation mit Android Jetpack Compose | Navigationsstrecken

Bei diesem Ansatz werden an einer zentralen Stelle, dem Router Module, die Navigationsstrecken definiert und die Navigationsausführungen mit entsprechender Anwendungslogik über das jeweilige ViewModel verwaltet und die ViewModel Logik für die Navigation über Dependency Injection aus den Composable Funktionen ausgeführt. Das Konzept wird nachfolgend beschrieben: 

Navigation mit Android Jetpack Compose | Dependency Injection

Ein zentrales Navigations Router Modul, in der die konkreten Navigationspfade über Sealed Classes definiert werden. Die Klasse NavTarget dient später als Parameter für den Navigator. Alle weiteren Feature Module werden hierarchisch als Sub-Sealed-Classes definiert. Dies ermöglicht über den Chain Operator und der automatischen Vorschläge auf der jeweiligen NavTarget Scope-Ebene durch IDE eine angenehme Navigation Pfad Auswahl. 

Für kleine bis mittelgroße Projekte reicht dies in der Regel aus, bei größeren Projekten wäre eine Separierung der Navigationsstrecken als eigenständige Sealed Classes je Feature-Modul eine Alternative. 

In der Enum-Klasse werden die Root-Module definiert, also der Einstiegspunkt in ein Modul, als auch die jeweiligen Feature-Screens in einem Feature-Modul. Wenn man eine Navigation zum Root-Modul angibt, leitet dieses innerhalb des Subgraphen zum jeweils frei definierbaren startDestination Feature Screen als Einstiegspunkt. Bei größeren Projekten kann ebenfalls eine Separierung je Feature-Modul erfolgen.   

Navigation mit Android Jetpack Compose | startDestination
Navigation mit Android Jetpack Compose | navTarget

Es wird eine Navigator-Komponente in dem Navigationsmodul erstellt, welches die Navigationsbefehle über einen SharedFlow entgegennimmt. 

Navigation mit Android Jetpack Compose | SharedFlow

Die entsprechenden Feature-Module implementieren jeweils das Navigationsmodul in ihrer build.gradle und die jeweiligen ViewModels injizieren den Navigator, um über die zuvor definierten Navigationspfade zu dem nächsten Pfad zu navigieren. Durch die zuvor erzeugte NavTarget Sealed-Class können die Navigationspfade problemlos wiederverwendet werden. Bei einer Änderung eines konkreten Navigationspfads lassen sich automatisch alle relevanten Stellen aktualisieren, was die Fehleranfälligkeit reduziert und die Nachvollziehbarkeit signifikant erhöht. Dieser Ansatz ist sowohl für statische Pfade als auch Pfade mit Übergabeparameter leicht realisierbar. 

Navigation mit Android Jetpack Compose | ThirdFeatureViewModel

Das App-Modul ist verantwortlich für die Erstellung des NavHostControllers 

Navigation mit Android Jetpack Compose | ComposeDemo1Theme

Erstellung des Navigationsgraphen in dem App-Modul, welches sich aus den einzelnen Subgraphen in den jeweiligen Feature-Modulen zusammensetzt. Der zentrale Navigationsgraph ermöglicht eine übersichtliche Zuordnung zu den Subgraphen. Zudem wird die SharedFlow Variable des Navigators in unserem Navigationsgraphen im App-Modul beobachtet, und bei Erhalt eines Werts die eigentliche Navigation über die Navigation Compose Bibliothek durchgeführt.

Navigation mit Android Jetpack Compose | NavigationComponent

Die jeweiligen Feature-Module erstellen Ihren Subgraphen, mit den jeweiligen Navigationspfaden, die für das Modul relevant sind. Der Navigationsgraph im App-Modul kennt alle Subgraphen der jeweiligen Feature-Module. 

Navigation mit Android Jetpack Compose | NavGraphBuilder

Die Composable-Funktionen können über die ViewModel Navigationsmethoden beliebig innerhalb des eigenen Feature-Moduls oder zu anderen Feature-Modulen navigieren.  
Zudem werden durch diesen flexiblen Ansatz zirkuläre Abhängigkeiten vermieden, wenn zwischen zwei Feature-Modulen hin und her navigiert werden soll. 

Navigation mit Android Jetpack Compose | ThirdFeatureContent

Dependency Injection Koin vs. Dagger Hilt

Für die Verwendung von Dependency Injection in Kombination mit Android Jetpack Compose und der Android Jetpack Compose Navigation, eignen sich mit Bezug auf die Komplexität sowohl Koin als auch Dagger Hilt gleichermaßen. 

Der wesentliche Unterschied liegt bei der Auflösung der Abhängigkeiten. 
Dagger Hilt löst Abhängigkeiten während der Kompilierung auf und Koin während der Ausführung.  

Navigation mit Android Jetpack Compose | NavGraphBuilder

Extra: Navigation über Burgermenü

Um die Navigation über ein Burgermenü zu realisieren, wird im Navigationsmodul eine Datenklasse erstellt, die zusätzlich zum NavTarget den Label Titel enthält 

Navigation mit Android Jetpack Compose | DrawerRoute

Anschließend lässt sich im DesignSystem Modul ein StandardBurgermenü erstellen 

Navigation mit Android Jetpack Compose | DefaultDrawer

Im ViewModel wird eine Funktion navigateToSpecificView erstellt, welches ein entsprechendes NavTarget als Übergabeparameter erhält, welches zuvor im Burgermenü angeklickt wurde (siehe onDestinationClicked). 

Navigation mit Android Jetpack Compose | navigateToSpecificView

Zur Nachvollziehbarkeit für ein späteres Debugging können die Navigationspfade in einer Basisklasse geloggt werden. 

Navigation mit Android Jetpack Compose | Basisklasse geloggt

Fazit

Mithilfe der Jetpack Navigation Compose Bibliothek in Verbindung mit dem flexiblen Navigationsmodul Ansatz ist eine Navigation zwischen den verschiedenen Composable-Funktionen über den verschiedenen Feature-Modulen strukturiert und übersichtlich möglich. Zudem lässt sich die Navigationslogik separat testen und Navigationspfade müssen lediglich einmalig definieren lassen. Zudem skaliert der Ansatz gut mit stetigem Hinzufügen von weiteren Feature-Modulen und ist weniger Fehleranfällig durch strikte Wiederverwendung. 

Weiterhin hat jedes Feature-Module seinen eigenen Subgraphen, in der das jeweilige Feature-Modul die Hoheit über die konkreten Navigationsstrecken der eigenen Feature-Screens verfügt.  

Zirkuläre Abhängigkeiten zwischen den Feature-Modulen aufgrund von möglicher bidirektionaler Navigation zwischen den Feature-Modulen werden durch den entkoppelten Ansatz über das Navigation Router Modul vermieden. 

Erfahren Sie mehr über unsere Leistungen

×
Telefon

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

+49 231 99953850
×