Jetpack Navigation Compose is the Jetpack Navigation library enhanced for usage in Compose. But the actual “enhancement” is a bit limited and different from the View-based navigation. Most importantly, there are no SafeArgs — a type-safe compile time solution for passing arguments between destinations. Navigation for Compose uses a simple URL pattern — arguments have to be converted to a string as a query parameter/path segment.
There are plenty of possible solutions — extensions to Jetpack Navigation Compose (Compose Destinations) or completely new navigation stacks with a custom implementation of the whole concept of navigation (Voyager, Compose Navigation Reimagined, Appyx). Yet every solution lacks something for our Kiwi.com apps usage.
There are also requirements that are supported in every navigation library, so let me rather enumerate key areas that are implemented differently or not implemented at all.
Safe argument passing — This is the main reason why we are not satisfied with the “raw” Jetpack Navigation Compose library, which we are currently using. We want to freely pass more complex types (Parcelable/Serializable), non-primitive types like lists of ints, or even nullable primitive types. All this is either impossible or extremely verbose and complex to set up.
Deeplink support and navigation up vs. navigation back — This is probably the greatest topic at all. Let’s take an example from our app and have this navigation flow:
Main screen -> Bookings tab -> Booking detail -> Booking passenger details
We are about to create a deep link to that passenger details screen. The screen requires a bookingId to show. In the case of deep link, one could imagine an URL like
example.com/bookings/123/passengers. The 123 is the bookingId. Opening this URL will open the app on the Booking passenger details screen. Then the user can:
- Navigate back using the swipe gesture/back button in the system navigation bar. This action returns the user to the app they were before — e.g. an email client where the user clicked the link.
- Navigate up using the back arrow in the toolbar/TopAppBar. This action goes to the previous screen in the navigation hierarchy — in this case to the actual Booking detail. What’s more important, the navigationUp should somehow “share” the bookingId from the current services screen and use it for opening the Booking detail screen.
Build speed — The final solution shouldn’t be using KAPT or KSP. Half of our modules are about UI and making them all KSP-dependent means a non-trivial slow-down. An acceptable approach can be using a Kotlin compiler plugin.
Dialogs, BottomSheets, Animations — One would expect that every library has solutions for dialogs, bottom-sheets, or custom animations/transitions. Actually, the Jetpack Navigation Compose library does not have bottom-sheets or custom animations/transitions, but there are official accompanying libraries — Accompanist’s Navigation Material BottomSheet and Navigation Animations. Simply, we don’t want to solve all those issues once again and expect the navigation library to provide solutions for us.
NavigationUp vs Back is well supported in Jetpack Navigation Compose (and therefore it Compose Destinations). Other custom navigation libraries are letting you define the deeplink’s back stack manually. The problematic part is that the developer has to manually track which back stack entry is “artificial” and therefore going back to this entry should rather navigate to previous app (in contrast to navigating up).
As mentioned earlier, KSP used in Compose Destination is a non-trivial slow-down that we do not want to sacrifice.
Dialogs and bottom sheets are somehow supported everywhere, though not always they are part of the navigation back stack and they can be modeled separately. Jetpack Navigation Compose seems to be the way we want to go with and it is somehow “promised” that bottom sheets will be supported natively.
Currently we do not miss much. Only the type-safe navigation. If only there was a mechanism in Kotlin that would allow us compile-time arguments processing and then runtime reflection-less conversion into an URL (and runtime reflection-less recreation from Bundle, respectively). Parcelables are quite limited. But wait, isn’t Kotlinx.Serilization exactly about this? Oh yes. That’s our solution.
NavigationComposeTyped is the name of our new open-sourced library, that solves the type-safe arguments passing problem using KotlinX.Serialization.
It is a simple set of extensions to the existing Jetpack Navigation Compose library. Those extension functions allow you to define destinations with arguments, easy navigation to those destinations, and the final arguments extraction. It utilizes the official KotlinX.Serialization Kotlin compiler plugin, therefore it is fast, and the plugin promises great Kotlin compatibility.
Let’s start hacking with a simple definition of the app’s destinations including their arguments:
Next, continue with the NavGraph construction
Finally, let’s allow Home to navigate
The library itself is basically a set of few simple public functions:
- to create a URL pattern for a Destination,
- to create navArguments definition for a Destination,
- to decode a Destination instance (arguments) from a received Bundle,
- to register a destination is a subtype of Destination (solving open polymorphism for KotlinX.Serialization).
Those functions help you implement type-safe navigation for other Jetpack Navigation Compose extensions, e.g. for bottom sheets from Accompanist’s library. Let’s take a look.
We had about 6+ NavHost using Navigation Compose already. Coming up with a solution that adds the missing piece helps us keep the continuity and knowledge. It allows us to freely integrate other Navigation Compose extensions.
We have just started. Check out the project and feel free to open an issue and discuss your experience and suggestions. The whole repository is open-sourced at github.com/kiwicom/navigation-compose-typed.