Type-safe navigation best practices with Jetpack Compose Navigation in multi-module projects

An overview of best practices starting with version 2.8.0

Frederick Klyk
5 min readJul 27, 2024
Photo by Willian Justen de Vasconcellos on Unsplash

Introduction to type-safe navigation with Jetpack Compose Navigation

With the release of Jetpack Compose Navigation 2.8.0-alpha08 on 01.05.2024, the ability to use types in navigation was introduced. This eliminates the need to pass strings as in the stable version, making the code less error-prone and allowing developers to benefit from typing and the advantages of the linter.

If navigation in Compose is still new, it is recommended to read the earlier article on the basic structures of Compose navigation,
see https://medium.com/@FrederickKlyk/effective-and-flexible-navigation-with-android-jetpack-compose-in-a-multimodular-project-13f4984ce979

With the update to version 2.8.0-alpha08 (see changelog:
https://developer.android.com/jetpack/androidx/releases/navigation?hl=de#2.8.0-alpha06) or higher and the integration of the Kotlin serialization plugin, classes can be serialized and thus made usable for the navigation framework.

Implementation of type-safe navigation with the Kotlin serialization plugin

The Kotlin Serialization Plugin is used for the implementation to provide serializable classes. This allows both simple data objects and more complex data classes to be defined as screens. The @Serializable annotation is crucial for preparing the classes for the transfer of arguments.

In contrast to other converters such as Gson, which are based on Java, Kotlin Serialization was developed specifically for Kotlin and works seamlessly with the language. In addition, Kotlin Serialization generates the serialization code at compile time, whereas Gson, for example, relies on runtime-reflection. This increases security, as serialization at compile time prevents runtime problems and crashes.

Define type-safe navigation paths for screens in Compose Navigation

As of Compose Navigation 2.8.0, screens can be defined as data objects and data classes. However, the encapsulation of the various screens in a sealed interface offers:

- Better clarity of the possible navigation paths,
- Generalization of the navigation paths,
- Avoidance of errors due to inadvertent specification of incorrect paths.

Within the sealed interface, simple data objects can be used for navigation to screens without arguments and data classes with parameters can be used for navigation with arguments.

Migration of the navigation code to Jetpack Compose Navigation 2.8.0

In versions prior to 2.8.0, the arguments for navigating to a new screen had to be extracted from strings, which was error-prone and confusing.

All parameters can now be retrieved directly from the corresponding data class of the screen. In addition, a significant amount of boilerplate code has been removed compared to the previous version, which concentrates the navigation logic on the essentials and makes the source code more readable/maintainable.
The specification of the startDestination in the NavHost Constructor now accepts custom types as well as strings, so that the data object or data class can be specified directly.
Compared to before, the destination path of the composable can now be specified as a generic with a custom type.

Multimodular architecture and type-safe nesting of navigation

In modern mobile apps, it makes sense to switch from a monolithic to a modular architecture from a certain project size. Each composable function can navigate within and across modules and exchange data securely between the modules.
The new type-safe navigation supports this approach and makes it easier to manage navigation routes.

A common problem with many screens is the lack of clarity in navigation. Here, it is possible to divide the screens within the respective modules into separate module graphs to ensure a clearer structure.
The NavGraphBuilder should then be used with a unique navigation module name for each screen.

For larger projects, it makes sense for reasons of clarity and traceability to outsource the type-safe navigation paths to their own module-granular Selead interfaces.
These should be defined with an identical prefix to ensure easy reuse. In addition, all module-granular sealed interfaces would inherit from a common sealed interface.

Transfer of complex data classes with Compose Navigation

In order to be able to pass complex data classes in addition to the primitive data types (String, Int, Boolean, etc.), the corresponding data class that is to be passed as a parameter must also have the @Parcelize annotation in addition to the @Serializable annotation and at the same time extend the class with Parcelable.

In order for the Compose Navigation Framework to understand how to serialize and deserialize this complex data class, a user-defined mapper of type NavType is required.
It is possible to write a generic mapper which, once written, can be reused for all different complex data class navigations.

The information can then be queried and further processed via the NavBackStackEntry as with the primitive data classes.

Advantages of type-safe navigation in Compose Navigation

- Reduced susceptibility to errors: The use of types instead of strings avoids typing errors and makes troubleshooting easier.
- Clarity and maintainability: The central declaration of navigation paths facilitates traceability and changeability.
- Reusability: Once defined, navigation paths can be easily reused, which reduces the susceptibility to errors and increases traceability.
- Central definition of navigation paths: Arguments and paths are defined and changed in one place and do not have to be adapted at various points in the navigation logic.

Best practices for navigation in Compose Navigation

- Do not include ViewModels in stateless composables screens: ViewModels should not be included in stateless composables in order to keep the UI logic testable.
- NavController not in stateless composable screens: Navigation logic and user interface should be strictly separated to keep the UI logic testable. The user interface should only trigger a corresponding navigation via a ViewModel method.
- Minimize the logic in navigation graphs: As far as possible, only necessary data (e.g. IDs) should be passed and the corresponding content loaded on the target screens.
- Decoupled navigation: The navigation logic should be decoupled from the dependencies between the feature modules. This avoids potentially complex circular dependencies.
- Use nested graphs in multi-modular projects: If possible, each module should be represented by its own navigation graph in order to achieve a clear separation and assignment of responsibilities and reduce complexity.
- Navigation routes in a central navigation module: The respective module routes should be defined in a central location for better generalization across different sealed interfaces. A prefix such as Navigation<ModulName> can be used as the interface name. The module interfaces in turn inherit from a common sealed interface.

Conclusion

Jetpack Compose Navigation 2.8.0 brings significant improvements for developers and increases the maturity level of the library.
The introduction of type-safe navigation and the ability to use data classes and objects reduce the susceptibility to errors and increase the maintainability and scalability of Android apps.
The new functions allow navigation paths to be designed clearly and robustly, which facilitates structuring in development overall.

Although Jetpack Compose Navigation 2.8.0 is still in the beta phase, the release of a stable version is possible at any time.

--

--

Frederick Klyk

Software Architect, Senior Android Developer & team leader @ adesso mobile solutions, Twitter: https://twitter.com/frederickklyk