Tag Archives: jetpack compose

Simplifying Navigation in Multi-Module Android Apps with Compose Destinations

In the world of Android development, we are often judged by the company and the app libraries we choose to keep.

Our journey at Meetup, developing both the “Meetup” and “Meetup for Organizers” apps, led us to an innovative solution for navigation in our multi-module apps: the library Compose Destinations. This blog aims to share our insights and to help guide others finding a simpler path.

The Lay of the Land

Our Android and iOS apps, built on Kotlin Multiplatform (KMP), share business logic, network, database, and resources code. To facilitate development, we’ve adopted a multi-module approach, segregating legacy code from new Compose and KMP modules. This structure, while beneficial, posed significant navigation challenges, prompting us to explore the multi-module capabilities of Compose Destinations.

We wanted to give up Android’s Activities and Fragments in our user interface code and instead use pure Jetpack Compose. Compose Destinations made this possible.

It’s the Journey, not the Destination

Compose Destinations stood out for its type-safe navigation and compatibility with the official Google library. Unlike Jetpack Compose Navigation, it offers built-in handling for returning data after navigation, simplifying our code and enhancing maintainability.

Unlike other Compose navigation libraries, Compose Destinations is built on top of the official Google library (Jetpack Compose Navigation) and merely supplements the official capabilities, rather than requiring that our engineers learn an entirely new third-party system for navigation.

Tying On Our Laces

Integrating Compose Destinations began with adding its Gradle plugin using KSP, along with the Compose Destinations animation-core library to enable smooth transitions. Each module contributing destinations required both an implementation dependency on the animation-core library and a ksp dependency on the library’s KSP plugin.

Our focus was on modules containing Compose code. The goal was straightforward navigation across these modules without engaging the legacy modules. This approach streamlined our integration.

Finding a Simpler Route

The biggest hurdle was the complexity of the official examples. We opted for a simpler, more intuitive approach that aligned better with our app’s structure and requirements.

We deviated from the complex nested navigation graphs presented in the official examples. Instead, we created a NavGraphs.kt for each app, defining startRoute and destinationsByRoute. Here’s a snippet from our build.gradle.kts file:

ksp {
    arg("compose-destinations.mode", "destinations")
    arg("compose-destinations.moduleName", "sharedAndroid")

At this point, we ran Gradle sync and generated code providing each app with all of the destinations from all modules.

Then we wrote our NavGraphs.kt file for each app, much like the following example:

object NavGraphs {
    val root = object : NavGraphSpec {
        val authRepository: AuthRepository by KoinJavaComponent.inject(AuthRepository::class.java)
        val profileRepository: ProfileRepository by KoinJavaComponent.inject(ProfileRepository::class.java)
        val isOrganizer = profileRepository.getProfile()?.isOrganizer ?: false

        override val route = "root"

        override val startRoute =
            if (authRepository.isLoggedIn() && isOrganizer) GroupScreenDestination
            else LoginDestination

        override val destinationsByRoute = (androidAppDestinations + sharedAndroidDestinations).associateBy { it.route }

You’ll notice that androidAppDestinations and sharedAndroidDestinations are generated Kotlin vals provided for us by the Compose Destinations KSP plugin. They’re named based upon the compose-destinations.moduleName specified above in the Gradle config. They’re only generated this way if compose-destinations.mode is set to destinations.

We then specify NavGraphs.root as the navGraph parameter to the DestinationsNavHost.

This setup enabled us to generate and access destinations across modules seamlessly, keeping our configuration minimal and easy to work with.

Where Did We End Up?

Transitioning from Fragments to Compose with Compose Destinations noticeably improved our apps’ performance compared to the old way using Fragments and Jetpack Navigation. Our staff immediately noticed how quickly new screens loaded on taps.

Our key takeaway is the importance of simplicity. While some apps might benefit from complex navigation graphs like the official examples, many can achieve optimal functionality with a more straightforward setup like ours.

Compose Destinations has been a game-changer for us, particularly with its type-safe navigation and efficient data handling. It has significantly improved the user experience, especially in cases where we would like to remember contextual state when navigating back to a previous screen.

Our experience integrating Compose Destinations has been overwhelmingly positive, offering a scalable, efficient navigation solution. We plan to continue refining our implementation and exploring new ways to leverage this powerful tool in our app development journey.

Remember, Remember, Jetpack Compose

During the first Kotlinconf in 2017, I asked Google for some kind of declarative user interface (UI) framework for Android. Specifically, I approached Stephanie Cuthbertson and Yigit Boyar at that San Francisco conference. I told them I liked the idea of Anko Layouts, the defunct framework developed by the Kotlin team at Jetbrains, but it really needed a solution with first party support and first class execution.

I’m sure many Android developers asked for something similar, just as many had asked Google for first-party Kotlin support in Android before they announced it at Google I/O 2017.

Today, Jetpack Compose not only delivers on my original request. It goes far above and beyond it. It solves the fragmentation problem of many Android OS versions showing UI widgets with different appearances. It fixed the issues of writing complex Adapters with so many kinds of rows for RecyclerViews. It even promotes stateless, functional UI and unidirectional data flow.

We’ve been working full-time in Jetpack Compose for almost six months now on our new Meetup for Organizers app and we love it.

Why I can’t remember()

However, I would like to focus in this blog on one particular, easily misused part of Jetpack Compose… the remember() function.

Here’s a TL;DR (too long; didn’t read) summary of what I’m about to write about the remember() function:

  1. remember() as little as possible. Just keep the exact pieces of data you really care about in Composable functions. The rest can be passed into your function.
  2. If you don’t want to lose state on rotation, use rememberSaveable(). Write a Saver only if necessary. @Parcelize can help.
  3. If the data is so large it could grow to hit a TransactionTooLargeException, you should persist that data and rememberSaveable() the keys instead.
  4. If you care about not losing data when the user leaves and returns to that screen, you should persist the data from the ViewModel and restore it when needed.

Ever since the beginning of Android development, memory and persistence has easily been one of the hardest challenges. Over the years, so many developers have had their apps break, crash, or forget data by screen rotation or by memory eviction. While Jetpack Compose is amazing, it doesn’t really eliminate these challenges.

One of the first tools Android developers learn when they start to explore Compose is the remember() function. And it’s likely one of the first they should forget. remember() has value when saving state in some notable circumstances, but it’s not great if your state is user input. I’ve seen very senior developers make basic errors when it comes to using remember().

remember() exists to save state in Composable functions between recompositions. However, it has many issues.

First of all, remember() doesn’t handle some configuration changes like rotation. If you rotate the screen, all remembered data on the screen is lost. There’s an easy fix for this called out immediately in the Compose docs. You can instead use rememberSaveable().

Our saving grace

Now, you’re probably thinking this whole blog is advocating that you should simply use rememberSaveable() and all problems are resolved, but that’s not my conclusion at all. Once you start using that new function, your solution can now cause three new problems.

If your state is complex, like a data class, sealed class, or class, then you now have to write a Saver for that class to explain how to bundle it. The quickest option is the @Parcelize annotation, but that might not be available in some circumstances.

Then there’s the TransactionTooLarge exception to contend with. Anyone who’s been writing Android apps for a long time knows that if you put too much into a Bundle, then save it to instance state to restore, you will run into this common crash. Anything you save with rememberSaveable() should be small and the entire Bundle should be small in aggregate.

Third, if your Composable function goes off of the screen, even for a moment, everything you remembered with rememberSaveable is lost. This is easy to encounter, for example, if you use Jetpack Compose Navigation and load a new route for a sub-screen. Let’s say your settings page needs a child page. When you return, your composable forgets everything you thought you remembered.

A pretty state of affairs

The solution to the last two problems is persistence. Ideally, you should be saving to your ViewModel or to a database, such as Room or SqlDelight (for Kotlin multi-platform projects). The only values which belong in rememberSaveable() when the state is very complicated are keys to query your database to retrieve the persisted data.

When you think about using the remember() function, just remember…

Do you care if data is lost on rotation? You shouldn’t use remember().

Is the data too large to store in an instance state? If so, you should be persisting data instead of simply using rememberSaveable().

Never forget. While Jetpack Compose is amazing and it makes our jobs as developers so much better, it doesn’t absolve us of concerns about persisting state.