Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ plugins {

android {
namespace = "android.template"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "android.template"
Expand Down Expand Up @@ -87,6 +87,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(project(":core:navigation"))

// Hilt Dependency Injection
implementation(libs.hilt.android)
Expand All @@ -101,7 +102,7 @@ dependencies {
// Arch Components
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
Expand Down
29 changes: 20 additions & 9 deletions app/src/main/java/android/template/ui/Navigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,32 @@

package android.template.ui

import android.template.core.navigation.Navigator
import android.template.core.navigation.Route
import android.template.ui.mymodel.MyModelScreen
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import android.template.ui.mymodel.MyModelScreen
import androidx.navigation3.NavDisplay
import androidx.navigation3.entryProvider
import kotlinx.serialization.Serializable

@Serializable
private data object MainRoute : Route.TopLevel

@Composable
fun MainNavigation() {
val navController = rememberNavController()
val navigator = remember { Navigator(startRoute = MainRoute) }

NavHost(navController = navController, startDestination = "main") {
composable("main") { MyModelScreen(modifier = Modifier.padding(16.dp)) }
// TODO: Add more destinations
}
NavDisplay(
backStack = navigator.backStack,
onBack = { navigator.goBack() },
entryProvider = entryProvider {
entry<MainRoute> {
MyModelScreen(modifier = Modifier.padding(16.dp))
}
},
)
}
29 changes: 29 additions & 0 deletions core/navigation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "android.template.core.navigation"
compileSdk = 36 // Should match app's compileSdk

defaultConfig {
minSdk = 21 // Should match app's minSdk
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}
}

dependencies {
// Nav3 runtime API
api(libs.androidx.navigation3.runtime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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
*
* https://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.
*/

package android.template.core.navigation

import android.annotation.SuppressLint
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.serialization.Serializable

/**
* Navigator that mirrors `NavController`'s back stack
*/
@SuppressLint("RestrictedApi")
class Navigator(
private val startRoute: Any = Unit,
private val canTopLevelRoutesExistTogether: Boolean = false,
private val shouldPrintDebugInfo: Boolean = false
) {

val backStack = mutableStateListOf(startRoute)
var topLevelRoute by mutableStateOf(startRoute)
private set

// Maintain a stack for each top level route
private lateinit var topLevelStacks : MutableMap<Any, MutableList<Any>>

// Maintain a map of shared routes to their parent stacks
private var sharedRoutes: MutableMap<Any, Any> = mutableMapOf()

val coroutineScope = CoroutineScope(Job())

init {
inititalizeTopLevelStacks()
}

private fun updateBackStack() {
backStack.apply {
clear()
val entries = topLevelStacks.flatMap { it.value }
addAll(entries)
}
printBackStack()
}

fun navlog(message: String){
if (shouldPrintDebugInfo){
println(message)
}
}

private fun printBackStack() {
navlog("Back stack: ${backStack.getDebugString()}")
}

private fun printTopLevelStacks() {

navlog("Top level stacks: ")
topLevelStacks.forEach { topLevelStack ->
navlog(" ${topLevelStack.key} => ${topLevelStack.value.getDebugString()}")
}
}

private fun List<Any>.getDebugString() : String {
val message = StringBuilder("[")
forEach { entry ->
if (entry is NavBackStackEntry){
message.append("Unmigrated route: ${entry.destination.route}, ")
} else {
message.append("Migrated route: $entry, ")
}
}
message.append("]\n")
return message.toString()
}

private fun addTopLevel(route: Any) {
if (route == startRoute) {
clearAllExceptStartStack()
} else {

// Get the existing stack or create a new one.
val topLevelStack = topLevelStacks.remove(route) ?: mutableListOf(route)

if (!canTopLevelRoutesExistTogether) {
clearAllExceptStartStack()
}

topLevelStacks.put(.route, topLevelStack)
navlog("Added top level route $route")
}
topLevelRoute = route
}

private fun clearAllExceptStartStack() {
// Remove all other top level stacks, except the start stack
val startStack = topLevelStacks[startRoute] ?: mutableListOf(startRoute)
topLevelStacks.clear()
topLevelStacks.put(startRoute, startStack)
}

private fun inititalizeTopLevelStacks() {
topLevelStacks = mutableMapOf(startRoute to mutableListOf(startRoute))
topLevelRoute = startRoute
}

private fun add(route: Any) {
navlog("Attempting to add $route")
if (route is Route.TopLevel) {
navlog("$route is a top level route")
addTopLevel(route)
} else {
if (route is Route.Shared) {
navlog("$route is a shared route")
// If the key is already in a stack, remove it
val oldParent = sharedRoutes[route]
if (oldParent != null) {
topLevelStacks[oldParent]?.remove(route)
}
sharedRoutes[route] = topLevelRoute
} else {
navlog("$route is a normal route")
}
val hasBeenAdded = topLevelStacks[topLevelRoute]?.add(route) ?: false
navlog("Added $route to $topLevelRoute stack: $hasBeenAdded")
}
}

/**
* Navigate to the given route.
*/
fun navigate(route: Any, navOptions: NavOptions? = null) {
add(route)
updateBackStack()
}

/**
* Go back to the previous route.
*/
fun goBack() {
if (backStack.size <= 1) {
return
}
val removedKey = topLevelStacks[topLevelRoute]?.removeLastOrNull()
// If the removed key was a top level key, remove the associated top level stack
topLevelStacks.remove(removedKey)
topLevelRoute = topLevelStacks.keys.last()
updateBackStack()
}
}

sealed interface Route {
interface TopLevel : Route
interface Shared : Route
}
8 changes: 7 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ androidxLifecycle = "2.8.7"
androidxActivity = "1.10.1"
androidxComposeBom = "2025.04.00"
androidxHilt = "1.2.0"
androidxNavigation = "2.8.9"
androidxNavigation = "2.9.3"
androidxNavigation3 = "1.0.0-alpha07"
androidxRoom = "2.7.0"
androidxTest = "1.6.1"
androidxTestExt = "1.2.1"
Expand All @@ -15,6 +16,7 @@ hilt = "2.56.1"
junit = "4.13.2"
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
kotlinSerialization = "2.1.20"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
Expand All @@ -31,6 +33,9 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
androidx-navigation-common-ktx = { module = "androidx.navigation:navigation-common-ktx", version.ref = "androidxNavigation" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidxNavigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidxNavigation3" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
Expand All @@ -51,5 +56,6 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
hilt-gradle = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ dependencyResolutionManagement {
}
rootProject.name = "MyApplication"

include(":app")
include(":app", ":core:navigation")
Loading