Jetpack Compose ist ein modernes Toolkit, mit dem native Android-UIs implementiert werden können. Es soll Entwickler:innen unterstützen, einfacher und schneller User Interfaces zu entwickeln, indem unter anderem weniger Code notwendig sein soll und intuitive Kotlin APIs genutzt werden können.
Ziel
Das Ziel dieses Blogbeitrags ist es, eine geeignete Architektur nach Best Practice anzubieten, um die Navigation einer Jetpack Compose BottomNavigationView optimal umsetzen zu können. Zudem werden für unterschiedliche Szenarien bei der Navigation entsprechende Implementierungsvorschläge dargestellt.
Um sich ein besseres Bild von der Ziel-Applikation machen zu können, soll dies im dargestellten Video näher veranschaulicht werden.
Wie im Video zu sehen ist, handelt es sich hierbei um eine reine Basisapplikation, die lediglich die Navigation mithilfe einer BottomNavigationView verdeutlichen soll.
Architekturansatz
Die in diesem Beitrag verfolgte Architektur baut auf die in dem Artikel präsentierten Architektur auf. Daher wäre es sinnvoll, sich vorab den verlinkten Blogbeitrag anzusehen, um den folgenden Implementierungen der Jetpack Compose BottomNavigationView besser folgen zu können.
Implementierung
Als Startpunkt der Implementierung nehmen wir die MainActivity. Diese sieht wie folgt aus:
Die MainActivity dient als Einstiegspunkt der App und enthält zunächst einmal einige Definitionen wie beispielsweise das Festlegen des Themes. Weitaus wichtiger ist hier die Instanziierung der NavigationComponent, die zum einen den NavController und zum anderen die Navigator-Instanz erhält. Letzteres ist eine selbst geschriebene Klasse, welche die Verantwortlichkeit für die Navigation zwischen den einzelnen Screens besitzt. Die NavigationComponent-Funktion sieht vorerst folgendermaßen aus:
Die Formulierung „vorerst“ ist deshalb gewählt, da man für unterschiedliche Verhaltensweisen der BottomNavigationView entsprechende Anpassungen tätigen muss – hier gehen wir nachfolgend näher darauf ein. Besonders deutlich wird anhand des Screenshots, dass in diesem Schritt der MainScreen erstellt wird, welcher die BottomNavigationView enthält.
Die BottomNavigationView kann wie auch andere Composable Functions üblicherweise erstellt werden. Allerdings wird als Container einer BottomNavigationView ein Scaffold benötigt, dem dann die BottomNavigationView, wie eingangs gezeigt wurde, übergeben werden kann. Innerhalb der BottomNavigationView werden im Anschluss die jeweiligen Items erstellt. Diese erhalten einen ClickListener, um zu dem entsprechenden Tab navigieren zu können. Dies wird über das ViewModel gelöst:
Das ViewModel nutzt dafür die in einer separaten File definierten Funktionen, welche die Navigation zu den einzelnen Screens umsetzen:
Dafür notwendig ist lediglich die Referenz zur Navigator-Instanz. Im MainScreen fehlt noch eine entscheidende Implementierung – der NavHost. Der NavHost ist für die eigentliche Navigation zuständig und beinhaltet sowohl das Instanziieren der einzelnen Screens als auch die Bekanntmachung der Routen und Labels von den Screens, unter denen diese erreichbar sind. Dieser bekommt neben dem NavController ebenfalls die Startroute übergeben. Innerhalb des NavHosts wird dann der NavGraph festgelegt, der für die gezeigte Basisapplikation folgendermaßen aussieht:
Auf Basis des NavGraphBuilders werden hier durch extension functions die einzelnen Routen und dazugehörigen Screens definiert. Die Verknüpfung zwischen den einzelnen Routen kann wie folgt veranschaulicht werden:
Der MainGraph kennt alle weiteren Subgraphen der einzelnen Tabs. Dadurch, dass die Basisapplikation sehr vereinfacht ist, besitzt jeder Subgraph ausschließlich den einen dazugehörigen TabScreen. Daher ist auch jeder TabScreen die startDestination des jeweiligen Graphen. Wie in der Implementierung der BottomNavigationView gezeigt wurde, wird dem NavHost nur der MainGraph übergeben, da dieser alle weiteren Routen und Screens kennt. Dies ermöglicht dann das Navigieren zwischen den einzelnen Tabs der BottomNavigationView mithilfe des ViewModels und den definierten separat ausgelagerten Hilfsfunktionen.
Verschiedene Anwendungsszenarien
Bei der BottomNavigationView hat man verschiedene Möglichkeiten, das Verhalten des Backstacks und dementsprechend der Navigation zwischen den einzelnen Tabs umzusetzen. Eine Möglichkeit wäre die hier dargestellte.
Das Video zeigt, dass bei jedem neuen Klick auf ein Item ein neuer TabScreen initiiert wird und so auf dem Backstack landet.
Navigiert man mithilfe des Zurück-Buttons auf den vorherigen Screen, so kann der gleiche Screen mehrfach auf dem Backstack liegen – allerdings als andere Instanz.
Dieses Verhalten ist nicht unbedingt falsch und kann gewissermaßen für einen entsprechenden Anwendungsfall sinnvoll sein – es wird durch die folgende Implementierung umgesetzt:
Sollte dieser Use Case nicht zutreffen, könnte man die im folgenden Video gezeigte Behandlung des Backstacks umsetzen. Hierbei wird keine neue Instanz bei erneutem Aufruf des Tabs erstellt, sondern die vorherige Referenz des Tabs verwendet.
Dies kann durch folgende Implementierung erreicht werden:
Eindeutiger wird dies, wenn der erste Tab als zweiter Tab verwendet wird.
Dieses Video zeigt, dass die Scrollposition des zweiten Tabs bestehen bleibt, obwohl dieser durch die BottomNavigationView erneut aufgerufen wurde.
Fazit
Der Beitrag hat aufgezeigt, dass die BottomNavigationView in Compose einfach zu integrieren ist. Die einzige Herausforderung besteht hier in der Überlegung der Navigation und der darunterliegenden Architektur. Plant man diese allerdings im Vorfeld und zieht verschiedene Betrachtungsweisen und Anwendungsfälle – wie beispielsweise das Verhalten des Backstacks – in Erwägung, so ist die Implementierung dessen leichter zu gestalten.