diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9e797c8..f3b153c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,10 +51,12 @@ dependencies { // UI implementation(libs.bundles.androidx.compose) + implementation(libs.androidx.activity) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.palette) + implementation(libs.androidx.savedstate) implementation(libs.androidx.tv.material) implementation(libs.coil.compose) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/data/Destinations.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/data/Destinations.kt index 974542a..f2f64c1 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/data/Destinations.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/data/Destinations.kt @@ -1,22 +1,27 @@ package nl.ndat.tvlauncher.data -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable +import androidx.compose.runtime.Composable import kotlinx.serialization.Serializable import nl.ndat.tvlauncher.ui.tab.apps.AppsTab import nl.ndat.tvlauncher.ui.tab.home.HomeTab +interface Destination { + @Composable + fun Content() +} + object Destinations { @Serializable - object Home + object Home : Destination { + @Composable + override fun Content() = HomeTab() + } @Serializable - object Apps + object Apps : Destination { + @Composable + override fun Content() = AppsTab() + } } val DefaultDestination = Destinations.Home - -fun NavGraphBuilder.createDestinationsGraph() { - composable { HomeTab() } - composable { AppsTab() } -} diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/screen/launcher/LauncherScreen.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/screen/launcher/LauncherScreen.kt index 85e1e9f..98cec33 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/screen/launcher/LauncherScreen.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/screen/launcher/LauncherScreen.kt @@ -1,7 +1,5 @@ package nl.ndat.tvlauncher.ui.screen.launcher -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -9,16 +7,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.compose.NavHost +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.ui.NavDisplay import nl.ndat.tvlauncher.ui.toolbar.Toolbar -import nl.ndat.tvlauncher.util.composition.LocalNavController -import nl.ndat.tvlauncher.util.composition.LocalNavGraph +import nl.ndat.tvlauncher.util.composition.LocalBackStack import nl.ndat.tvlauncher.util.composition.ProvideNavigation import nl.ndat.tvlauncher.util.modifier.autoFocus @Composable fun LauncherScreen() { - ProvideNavigation() { + ProvideNavigation { Column( modifier = Modifier .fillMaxSize() @@ -35,11 +33,14 @@ fun LauncherScreen() { modifier = Modifier .autoFocus() ) { - NavHost( - navController = LocalNavController.current, - graph = LocalNavGraph.current, - enterTransition = { fadeIn() }, - exitTransition = { fadeOut() } + val backStack = LocalBackStack.current + NavDisplay( + backStack = backStack, + entryProvider = { route -> + NavEntry(route) { + route.Content() + } + }, ) } } diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/toolbar/ToolbarTabs.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/toolbar/ToolbarTabs.kt index 8cb4334..811e36e 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/ui/toolbar/ToolbarTabs.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/ui/toolbar/ToolbarTabs.kt @@ -2,56 +2,44 @@ package nl.ndat.tvlauncher.ui.toolbar import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.get import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.Text -import kotlinx.coroutines.flow.map import nl.ndat.tvlauncher.R import nl.ndat.tvlauncher.data.Destinations -import nl.ndat.tvlauncher.util.composition.LocalNavController -import nl.ndat.tvlauncher.util.composition.LocalNavGraph +import nl.ndat.tvlauncher.util.composition.LocalBackStack @OptIn(ExperimentalComposeUiApi::class) @Composable fun ToolbarTabs( modifier: Modifier, ) { - val navController = LocalNavController.current - val navGraph = LocalNavGraph.current - val currentDestinationId by navController.currentBackStackEntryFlow.map { it.destination.id }.collectAsState(null) + val backStack = LocalBackStack.current + val currentDestination = backStack.lastOrNull() val tabs = mapOf( Destinations.Home to stringResource(R.string.tab_home), Destinations.Apps to stringResource(R.string.tab_apps), ) TabRow( - selectedTabIndex = tabs.keys.indexOfFirst { destination -> navGraph[destination].id == currentDestinationId }, + selectedTabIndex = tabs.keys.indexOfFirst { destination -> destination == currentDestination }, modifier = modifier.focusRestorer(), ) { tabs.toList().forEachIndexed { index, (destination, name) -> key(index) { - val selected = navGraph[destination].id == currentDestinationId Tab( - selected = selected, + selected = destination == currentDestination, onFocus = { - if (!selected) { - navController.navigate(destination) { - if (currentDestinationId != null) { - popUpTo(currentDestinationId!!) { - inclusive = true - } - } - } - } + if (destination == currentDestination) return@Tab + if (currentDestination != Destinations.Home) backStack.removeLastOrNull() + + backStack.add(destination) }, modifier = Modifier.padding(16.dp, 8.dp) ) { diff --git a/app/src/main/kotlin/nl/ndat/tvlauncher/util/composition/navigation.kt b/app/src/main/kotlin/nl/ndat/tvlauncher/util/composition/navigation.kt index 5813e3a..526ef7d 100644 --- a/app/src/main/kotlin/nl/ndat/tvlauncher/util/composition/navigation.kt +++ b/app/src/main/kotlin/nl/ndat/tvlauncher/util/composition/navigation.kt @@ -2,34 +2,21 @@ package nl.ndat.tvlauncher.util.composition import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.staticCompositionLocalOf -import androidx.navigation.NavGraph -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import androidx.navigation.createGraph import nl.ndat.tvlauncher.data.DefaultDestination -import nl.ndat.tvlauncher.data.createDestinationsGraph +import nl.ndat.tvlauncher.data.Destination -val LocalNavController = staticCompositionLocalOf { - error("NavController not provided") +val LocalBackStack = staticCompositionLocalOf> { + error("LocalBackStack not provided") } - -val LocalNavGraph = staticCompositionLocalOf { - error("NavGraph not provided") -} - @Composable fun ProvideNavigation( - navController: NavHostController = rememberNavController(), - navGraph: NavGraph = remember(navController) { - navController.createGraph(DefaultDestination) { - createDestinationsGraph() - } - }, + backStack: SnapshotStateList = remember { mutableStateListOf(DefaultDestination) }, content: @Composable () -> Unit, ) = CompositionLocalProvider( - LocalNavController provides navController, - LocalNavGraph provides navGraph, + LocalBackStack provides backStack, content = content, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a81e245..a0f040b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,14 +3,16 @@ android-compileSdk = "36" android-minSdk = "21" android-plugin = "8.10.1" android-targetSdk = "36" -androidx-activity = "1.10.1" +androidx-activity = "1.12.0-alpha03" androidx-appcompat = "1.7.1" -androidx-compose-foundation = "1.8.3" -androidx-compose-ui = "1.8.3" +androidx-compose-foundation = "1.9.0-beta01" +androidx-compose-runtime = "1.9.0-beta01" +androidx-compose-ui = "1.9.0-beta01" androidx-core = "1.16.0" androidx-core-role = "1.1.0" -androidx-navigation = "2.9.0" +androidx-navigation3 = "1.0.0-alpha04" androidx-palette = "1.0.0" +androidx-savedstate = "1.3.0" androidx-tv = "1.0.0" androidx-tvprovider = "1.1.0" coil = "2.7.0" @@ -30,18 +32,20 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } [libraries] +androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-foundation" } +androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "androidx-compose-runtime" } +androidx-compose-runtime-savable = { module = "androidx.compose.runtime:runtime-saveable", version.ref = "androidx-compose-runtime" } androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidx-compose-ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose-ui" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose-ui" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-core-role = { module = "androidx.core:core-role", version.ref = "androidx-core-role" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation" } -androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "androidx-navigation" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } androidx-palette = { module = "androidx.palette:palette-ktx", version.ref = "androidx-palette" } +androidx-savedstate = { module = "androidx.savedstate:savedstate", version.ref = "androidx-savedstate" } androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "androidx-tv" } androidx-tvprovider = { module = "androidx.tvprovider:tvprovider", version.ref = "androidx-tvprovider" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } @@ -55,6 +59,8 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } [bundles] androidx-compose = [ "androidx-compose-foundation", + "androidx-compose-runtime", + "androidx-compose-runtime-savable", "androidx-compose-ui", "androidx-compose-ui-tooling-preview", ]