A clean, scalable approach to Jetpack Compose navigation in multi-module Android apps using Navigation 3 (androidx.navigation3).
- Type-safe navigation with Kotlinx Serialization
- Module independence - features depend only on shared
navigationmodule - Navigation results - pass data back when navigating up (like Activity result API)
- Bottom tab navigation - tab roots preserved in a single back stack
app/ # Coordinates navigation and UI
navigation/ # Shared abstractions (Navigator, AppScreen, NavigationResults)
home/ # Feature module
search/ # Feature module
profile/ # Feature module
details/ # Feature module
Core Concepts:
AppComposeNavigator- Navigation interface withnavigate(),navigateUp(),popUpTo(),navigateBackWithResult()AppScreen- Sealed interface for all destinations (uses@Serializablefor type-safe arguments)NavigationResults- Type-safe result passing between screens
// Define destination
@Serializable
data class Details(
val placeholderId: String,
val initialIsFavorite: Boolean,
val initialIsInCart: Boolean,
) : AppScreen
// Navigate
navigator.navigate(
Details(
placeholderId = "item-123",
initialIsFavorite = true,
initialIsInCart = false
)
)Send result:
navigator.navigateBackWithResult(
key = Details.RESULT_KEY,
result = Details.Result(
placeholderId = placeholderId,
isFavorite = isFavorite,
isInCart = isInCart,
)
)Receive result:
LaunchedEffect(results) {
results.resultFlow<Details.Result>(Details.RESULT_KEY)
.collect { result ->
viewModel.onItemStateChanged(result)
results.clear(Details.RESULT_KEY) // Must clear to avoid re-delivery
}
}Best Practice: Keep result keys and types inside the destination definition (e.g., Details.RESULT_KEY and Details.Result) to create clear contracts.
fun appEntryProvider(...) = entryProvider {
entry<HomeTab> { HomeTabRoute(navigator, results) }
entry<Details> { key -> DetailsRoute(navigator, key.placeholderId, ...) }
}- Type safety - Compile-time checking, refactoring-friendly
- Module independence - Feature modules only depend on
navigationabstractions - Clear contracts - Destinations own their keys and result types
- Testable - Navigator is an interface, easy to mock
- Scalable - Navigation logic centralized, easy to reason about
results.clear(key) after consumption to avoid re-delivery.
app module
├─→ navigation (shared contracts)
├─→ home
├─→ search
├─→ profile
└─→ details
Feature modules → navigation only (for AppScreen, Navigator, NavigationResults)
Feature modules never depend on each other.
Copyright 2026 Kyriakos Georgiopoulos
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Author: Kyriakos Georgiopoulos
⭐ Star this repo if you find it helpful!