Skip to content

Feature/template 194 navigation3#196

Open
ninovanhooff wants to merge 15 commits intodevelopfrom
feature/template-194-navigation3
Open

Feature/template 194 navigation3#196
ninovanhooff wants to merge 15 commits intodevelopfrom
feature/template-194-navigation3

Conversation

@ninovanhooff
Copy link
Collaborator

@ninovanhooff ninovanhooff commented Jan 9, 2026

Why is this important?

implements #194

Notes

I chose not to use the Koin-Navigation3 integration, because it only gives very small benefits in developer ergonomics, while you tightly integrate DI with nanigation (you have to use the Navigator provided by Koin, while we prefer to have that separately implemented)

todo

  • deeplinks link
  • cleanup
  • update the bottom navigation branch

feature/template-194-navigation3
feature/template-194-navigation3
feature/template-194-navigation3
Increases consistency

feature/template-194-navigation3
feature/template-194-navigation3
…94-navigation3

# Conflicts:
#	app/src/main/kotlin/nl/q42/template/MainActivity.kt
#	gradle/libs.versions.toml
feature/template-194-navigation3
feature/template-194-navigation3
feature/template-194-navigation3
feature/template-194-navigation3
feature/template-194-navigation3
feature/template-194-navigation3
@ninovanhooff ninovanhooff marked this pull request as ready for review February 13, 2026 19:20
feature/template-194-navigation3
feature/template-194-navigation3
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a migration from Navigation Compose 2.x to Navigation 3, a major refactoring of the app's navigation system. The migration introduces a custom navigation state management solution that avoids tight coupling with Koin's Navigator, providing more flexibility while maintaining the existing navigation patterns. The PR includes comprehensive deeplink support using Navigation 3's serialization-based approach.

Changes:

  • Migrated from androidx.navigation:navigation-compose to androidx.navigation3 libraries (runtime and ui)
  • Replaced navigation graphs with entry provider scopes and custom Navigator class for back stack management
  • Implemented deeplink parsing infrastructure with pattern matching and key decoding
  • Updated ViewModels to receive Destination parameters via Koin's parametersOf mechanism

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Updated Koin to 4.2.0-RC1, added Navigation 3 libraries, added koin-annotations, removed old composeNavigation dependency
feature/home/src/main/kotlin/nl/q42/template/home/second/presentation/HomeSecondViewModel.kt Replaced SavedStateHandle with @provided Destination.HomeSecond parameter
core/navigation/src/main/kotlin/nl/q42/template/navigation/viewmodel/RouteNavigator.kt Renamed navigationState to appNavigationState and NavigationState to AppNavigationState
core/navigation/src/main/kotlin/nl/q42/template/navigation/viewmodel/Navigator3.kt New Navigator class that manages navigation by manipulating back stacks directly
core/navigation/src/main/kotlin/nl/q42/template/navigation/viewmodel/NavigationState.kt Complete rewrite - now manages Navigation 3 back stacks and converts them to NavEntries
core/navigation/src/main/kotlin/nl/q42/template/navigation/viewmodel/InitNavigator.kt Updated to work with new Navigator class instead of NavHostController
core/navigation/src/main/kotlin/nl/q42/template/navigation/viewmodel/AppNavigationState.kt New file containing the old NavigationState and BackstackBehavior definitions
core/navigation/src/main/kotlin/nl/q42/template/navigation/Destinations.kt Made Destination sealed class extend NavKey interface
core/navigation/src/main/kotlin/nl/q42/template/navigation/AppGraphRoutes.kt Removed file - no longer needed with Navigation 3's entry-based approach
build.dep.navigation.gradle Updated to use Navigation 3 libraries
build.dep.di.gradle Added koin-annotations dependency
app/src/main/kotlin/nl/q42/template/navigation/deeplink/KeyDecoder.kt New decoder for deserializing deeplink arguments into Destination objects
app/src/main/kotlin/nl/q42/template/navigation/deeplink/DeepLinkRequest.kt New class to parse and store deeplink URIs in a structured format
app/src/main/kotlin/nl/q42/template/navigation/deeplink/DeepLinkPattern.kt New class to parse and validate deeplink patterns against Destination schemas
app/src/main/kotlin/nl/q42/template/navigation/deeplink/DeepLinkParser.kt New parser that matches incoming deeplink intents to registered patterns
app/src/main/kotlin/nl/q42/template/navigation/deeplink/DeepLinkMatcher.kt New matcher that compares requested deeplinks against supported patterns
app/src/main/kotlin/nl/q42/template/navigation/OnboardingDestinations.kt Converted from NavGraphBuilder extension to EntryProviderScope extension
app/src/main/kotlin/nl/q42/template/navigation/HomeGraph.kt Removed - replaced by HomeEntry.kt
app/src/main/kotlin/nl/q42/template/navigation/HomeEntry.kt New file defining home destination entries using Navigation 3 patterns
app/src/main/kotlin/nl/q42/template/di/AppModule.kt Added DeeplinkParser as singleton
app/src/main/kotlin/nl/q42/template/MainActivity.kt Replaced NavHost with NavDisplay and integrated deeplink parsing at startup
app/build.gradle Removed old composeNavigation dependency
.idea/copilotDiffState.xml IDE-specific file that should not be in version control
Files not reviewed (1)
  • .idea/copilotDiffState.xml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

InitNavigator(navController = navController, viewModel)
internal fun EntryProviderScope<NavKey>.onboardingEntry(navigator: Navigator) {
entry<Destination.Onboarding> { key ->
val viewModel: OnboardingStartViewModel = koinViewModel { parametersOf(key) }
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to HomeViewModel, OnboardingStartViewModel does not accept a Destination.Onboarding parameter but parametersOf(key) is being passed during its construction. This will cause a DI injection failure at runtime. Either remove the parametersOf(key) call or add a @Provided params: Destination.Onboarding parameter to OnboardingStartViewModel if the destination data is needed.

Suggested change
val viewModel: OnboardingStartViewModel = koinViewModel { parametersOf(key) }
val viewModel: OnboardingStartViewModel = koinViewModel()

Copilot uses AI. Check for mistakes.
PrimitiveKind.INT -> String::toInt
PrimitiveKind.BOOLEAN -> String::toBoolean
PrimitiveKind.BYTE -> String::toByte
PrimitiveKind.CHAR -> String::toCharArray
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PrimitiveKind.CHAR type parser returns String::toCharArray which produces a CharArray, not a Char. This will cause a type mismatch when trying to decode a character parameter. The correct parser should be { it.single() } to get a single Char from the string.

Suggested change
PrimitiveKind.CHAR -> String::toCharArray
PrimitiveKind.CHAR -> { it.single() }

Copilot uses AI. Check for mistakes.
@@ -1,3 +1,4 @@
dependencies {
implementation(libs.koin)
implementation(libs.koin.annotations)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The koin-annotations library is being added to the dependencies but it's only used in one place (@Provided annotation in HomeSecondViewModel). Additionally, the @Provided annotation usage appears to be incorrect - this annotation is part of Koin's code generation system which requires additional setup with KSP processor. Without the processor configured, this annotation won't work as intended. Consider either properly setting up Koin annotations with KSP, or using the standard Koin parametersOf mechanism without the annotation.

Suggested change
implementation(libs.koin.annotations)

Copilot uses AI. Check for mistakes.
navController.navigateUp()
onNavigated(navigationState)
is AppNavigationState.NavigateUp -> {
navigator.goBack()
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NavigateUp case is missing a call to onNavigated(appNavigationState) after calling navigator.goBack(). This inconsistency with the other navigation cases means the navigation state won't be properly reset to Idle after a back navigation, potentially causing the same navigation action to be triggered multiple times.

Suggested change
navigator.goBack()
navigator.goBack()
onNavigated(appNavigationState)

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +18
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CopilotDiffPersistence">
<option name="pendingDiffs">
<map>
<entry key="$PROJECT_DIR$/gradle/libs.versions.toml">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/gradle/libs.versions.toml" />
<option name="originalContent" value="[versions]&#10;jvmTarget = &quot;21&quot;&#10;kotlin = &quot;2.2.20&quot;&#10;# should match kotlin https://github.com/google/ksp/releases&#10;gradlePlugin = &quot;8.11.2&quot;&#10;googleServices = &quot;4.4.4&quot;&#10;crashlyticsPlugin = &quot;3.0.6&quot;&#10;firebaseBOM = &quot;34.4.0&quot;&#10;retrofit = &quot;3.0.0&quot;&#10;kotlinx-serialization = &quot;1.9.0&quot;&#10;retrofit2KotlinxSerializationConverter = &quot;1.0.0&quot;&#10;networkResponseAdapter = &quot;5.0.0&quot;&#10;napier = &quot;2.7.1&quot;&#10;composeNavigation = &quot;2.9.5&quot;&#10;okhttp = &quot;5.2.1&quot;&#10;composePlatform = &quot;2025.10.00&quot;&#10;activityCompose = &quot;1.11.0&quot;&#10;composeLifecycle = &quot;2.9.4&quot;&#10;# Test dependencies&#10;kotlinxCoroutinesTest = &quot;1.10.2&quot;&#10;junit = &quot;4.13.2&quot;&#10;mockkAndroid = &quot;1.14.6&quot;&#10;turbine = &quot;1.2.1&quot;&#10;composeStateEvents = &quot;2.2.0&quot;&#10;koin = &quot;4.1.1&quot;&#10;&#10;[libraries]&#10;junit = { module = &quot;junit:junit&quot;, version.ref = &quot;junit&quot; }&#10;kotlin-test = { module = &quot;org.jetbrains.kotlin:kotlin-test&quot; }&#10;kotlinx-coroutines-test = { module = &quot;org.jetbrains.kotlinx:kotlinx-coroutines-test&quot;, version.ref = &quot;kotlinxCoroutinesTest&quot; }&#10;mockk-agent = { module = &quot;io.mockk:mockk-agent&quot;, version.ref = &quot;mockkAndroid&quot; }&#10;mockk-android = { module = &quot;io.mockk:mockk-android&quot;, version.ref = &quot;mockkAndroid&quot; }&#10;retrofit = { module = &quot;com.squareup.retrofit2:retrofit&quot;, version.ref = &quot;retrofit&quot; }&#10;retrofit2-kotlinx-serialization-converter = { module = &quot;com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter&quot;, version.ref = &quot;retrofit2KotlinxSerializationConverter&quot; }&#10;kotlinx-serialization-json = { module = &quot;org.jetbrains.kotlinx:kotlinx-serialization-json&quot;, version.ref = &quot;kotlinx-serialization&quot; }&#10;networkResponseAdapter = { module = &quot;com.github.haroldadmin:NetworkResponseAdapter&quot;, version.ref = &quot;networkResponseAdapter&quot; }&#10;okhttp = { module = &quot;com.squareup.okhttp3:okhttp&quot;, version.ref = &quot;okhttp&quot; }&#10;okhttpLogging = { module = &quot;com.squareup.okhttp3:logging-interceptor&quot;, version.ref = &quot;okhttp&quot; }&#10;napier = { module = &quot;io.github.aakira:napier&quot;, version.ref = &quot;napier&quot; }&#10;composeUIGraphics = { module = &quot;androidx.compose.ui:ui-graphics&quot; }&#10;composeUITooling = { module = &quot;androidx.compose.ui:ui-tooling&quot; }&#10;composeUIToolingPreview = { module = &quot;androidx.compose.ui:ui-tooling-preview&quot; }&#10;composeMaterial3 = { module = &quot;androidx.compose.material3:material3&quot; }&#10;composePlatform = { module = &quot;androidx.compose:compose-bom&quot;, version.ref = &quot;composePlatform&quot; }&#10;firebaseBoM = { module = &quot;com.google.firebase:firebase-bom&quot;, version.ref = &quot;firebaseBOM&quot; }&#10;firebaseCrashlytics = { module = &quot;com.google.firebase:firebase-crashlytics&quot; }&#10;activityCompose = { module = &quot;androidx.activity:activity-compose&quot;, version.ref = &quot;activityCompose&quot; }&#10;composeLifecycle = { module = &quot;androidx.lifecycle:lifecycle-runtime-compose&quot;, version.ref = &quot;composeLifecycle&quot; }&#10;turbine = { module = &quot;app.cash.turbine:turbine&quot;, version.ref = &quot;turbine&quot; }&#10;composeStateEvents = { module = &quot;com.github.leonard-palm:compose-state-events&quot;, version.ref = &quot;composeStateEvents&quot; }&#10;composeNavigation = { module = &quot;androidx.navigation:navigation-compose&quot;, version.ref = &quot;composeNavigation&quot; }&#10;koin = { module = &quot;io.insert-koin:koin-android&quot;, version.ref = &quot;koin&quot; }&#10;koin-compose = { module = &quot;io.insert-koin:koin-androidx-compose&quot;, version.ref = &quot;koin&quot; }&#10;koin-test = { module = &quot;io.insert-koin:koin-test-junit4&quot;, version.ref = &quot;koin&quot; }&#10;&#10;[plugins]&#10;androidApplication = { id = &quot;com.android.application&quot;, version.ref = &quot;gradlePlugin&quot; }&#10;androidLibrary = { id = &quot;com.android.library&quot;, version.ref = &quot;gradlePlugin&quot; }&#10;kotlinSerialization = { id = &quot;org.jetbrains.kotlin.plugin.serialization&quot;, version.ref = &quot;kotlin&quot; }&#10;jetbrainsKotlinAndroid = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }&#10;googleServices = { id = &quot;com.google.gms.google-services&quot;, version.ref = &quot;googleServices&quot; }&#10;firebaseCrashlyticsPlugin = { id = &quot;com.google.firebase.crashlytics&quot;, version.ref = &quot;crashlyticsPlugin&quot; }&#10;compose-compiler = { id = &quot;org.jetbrains.kotlin.plugin.compose&quot;, version.ref = &quot;kotlin&quot; }&#10;" />
<option name="updatedContent" value="[versions]&#10;jvmTarget = &quot;21&quot;&#10;kotlin = &quot;2.2.20&quot;&#10;# should match kotlin https://github.com/google/ksp/releases&#10;gradlePlugin = &quot;8.11.2&quot;&#10;googleServices = &quot;4.4.4&quot;&#10;crashlyticsPlugin = &quot;3.0.6&quot;&#10;firebaseBOM = &quot;34.4.0&quot;&#10;retrofit = &quot;3.0.0&quot;&#10;kotlinx-serialization = &quot;1.9.0&quot;&#10;retrofit2KotlinxSerializationConverter = &quot;1.0.0&quot;&#10;networkResponseAdapter = &quot;5.0.0&quot;&#10;napier = &quot;2.7.1&quot;&#10;composeNavigation = &quot;2.9.5&quot;&#10;okhttp = &quot;5.2.1&quot;&#10;composePlatform = &quot;2025.10.00&quot;&#10;activityCompose = &quot;1.11.0&quot;&#10;composeLifecycle = &quot;2.9.4&quot;&#10;# Test dependencies&#10;kotlinxCoroutinesTest = &quot;1.10.2&quot;&#10;junit = &quot;4.13.2&quot;&#10;mockkAndroid = &quot;1.14.6&quot;&#10;turbine = &quot;1.2.1&quot;&#10;composeStateEvents = &quot;2.2.0&quot;&#10;koin = &quot;4.1.1&quot;&#10;&#10;[libraries]&#10;junit = { module = &quot;junit:junit&quot;, version.ref = &quot;junit&quot; }&#10;kotlin-test = { module = &quot;org.jetbrains.kotlin:kotlin-test&quot; }&#10;kotlinx-coroutines-test = { module = &quot;org.jetbrains.kotlinx:kotlinx-coroutines-test&quot;, version.ref = &quot;kotlinxCoroutinesTest&quot; }&#10;mockk-agent = { module = &quot;io.mockk:mockk-agent&quot;, version.ref = &quot;mockkAndroid&quot; }&#10;mockk-android = { module = &quot;io.mockk:mockk-android&quot;, version.ref = &quot;mockkAndroid&quot; }&#10;retrofit = { module = &quot;com.squareup.retrofit2:retrofit&quot;, version.ref = &quot;retrofit&quot; }&#10;retrofit2-kotlinx-serialization-converter = { module = &quot;com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter&quot;, version.ref = &quot;retrofit2KotlinxSerializationConverter&quot; }&#10;kotlinx-serialization-json = { module = &quot;org.jetbrains.kotlinx:kotlinx-serialization-json&quot;, version.ref = &quot;kotlinx-serialization&quot; }&#10;networkResponseAdapter = { module = &quot;com.github.haroldadmin:NetworkResponseAdapter&quot;, version.ref = &quot;networkResponseAdapter&quot; }&#10;okhttp = { module = &quot;com.squareup.okhttp3:okhttp&quot;, version.ref = &quot;okhttp&quot; }&#10;okhttpLogging = { module = &quot;com.squareup.okhttp3:logging-interceptor&quot;, version.ref = &quot;okhttp&quot; }&#10;napier = { module = &quot;io.github.aakira:napier&quot;, version.ref = &quot;napier&quot; }&#10;composeUIGraphics = { module = &quot;androidx.compose.ui:ui-graphics&quot; }&#10;composeUITooling = { module = &quot;androidx.compose.ui:ui-tooling&quot; }&#10;composeUIToolingPreview = { module = &quot;androidx.compose.ui:ui-tooling-preview&quot; }&#10;composeMaterial3 = { module = &quot;androidx.compose.material3:material3&quot; }&#10;composePlatform = { module = &quot;androidx.compose:compose-bom&quot;, version.ref = &quot;composePlatform&quot; }&#10;firebaseBoM = { module = &quot;com.google.firebase:firebase-bom&quot;, version.ref = &quot;firebaseBOM&quot; }&#10;firebaseCrashlytics = { module = &quot;com.google.firebase:firebase-crashlytics&quot; }&#10;activityCompose = { module = &quot;androidx.activity:activity-compose&quot;, version.ref = &quot;activityCompose&quot; }&#10;composeLifecycle = { module = &quot;androidx.lifecycle:lifecycle-runtime-compose&quot;, version.ref = &quot;composeLifecycle&quot; }&#10;turbine = { module = &quot;app.cash.turbine:turbine&quot;, version.ref = &quot;turbine&quot; }&#10;composeStateEvents = { module = &quot;com.github.leonard-palm:compose-state-events&quot;, version.ref = &quot;composeStateEvents&quot; }&#10;composeNavigation = { module = &quot;androidx.navigation:navigation-compose&quot;, version.ref = &quot;composeNavigation&quot; }&#10;koin = { module = &quot;io.insert-koin:koin-android&quot;, version.ref = &quot;koin&quot; }&#10;koin-compose = { module = &quot;io.insert-koin:koin-androidx-compose&quot;, version.ref = &quot;koin&quot; }&#10;koin-test = { module = &quot;io.insert-koin:koin-test-junit4&quot;, version.ref = &quot;koin&quot; }&#10;&#10;[plugins]&#10;androidApplication = { id = &quot;com.android.application&quot;, version.ref = &quot;gradlePlugin&quot; }&#10;androidLibrary = { id = &quot;com.android.library&quot;, version.ref = &quot;gradlePlugin&quot; }&#10;kotlinSerialization = { id = &quot;org.jetbrains.kotlin.plugin.serialization&quot;, version.ref = &quot;kotlin&quot; }&#10;jetbrainsKotlinAndroid = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }&#10;googleServices = { id = &quot;com.google.gms.google-services&quot;, version.ref = &quot;googleServices&quot; }&#10;firebaseCrashlyticsPlugin = { id = &quot;com.google.firebase.crashlytics&quot;, version.ref = &quot;crashlyticsPlugin&quot; }&#10;compose-compiler = { id = &quot;org.jetbrains.kotlin.plugin.compose&quot;, version.ref = &quot;kotlin&quot; }" />
</PendingDiffInfo>
</value>
</entry>
</map>
</option>
</component>
</project> No newline at end of file
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .idea/copilotDiffState.xml file should not be included in version control. This is an IDE-specific configuration file that contains pending diff information and is typically user-specific. It should be added to .gitignore to prevent it from being tracked.

Copilot uses AI. Check for mistakes.

internal fun EntryProviderScope<NavKey>.homeEntry(navigator: Navigator) {
entry<Destination.Home> { key ->
val viewModel: HomeViewModel = koinViewModel { parametersOf(key) }
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @Provided annotation is being used to mark the params parameter, but HomeViewModel does not accept a Destination.Home parameter and will fail when Koin tries to inject it with parametersOf(key). This inconsistency between HomeSecondViewModel (which has a params parameter) and HomeViewModel (which doesn't) needs to be resolved. Either remove the parametersOf(key) call in HomeEntry.kt for HomeViewModel, or add a @Provided params: Destination.Home parameter to HomeViewModel if the destination data is needed.

Suggested change
val viewModel: HomeViewModel = koinViewModel { parametersOf(key) }
val viewModel: HomeViewModel = koinViewModel()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant