refactor(folio): replace custom Navigator with NavHost backstack

Wraps androidx.navigation.NavHostController behind the existing
push/replace/back surface so call sites in ViewModels stay unchanged.
App.kt now wires a typed NavHost with @Serializable Route entries
and observes the controller's currentBackStackEntry to drive the
session-based Login/Home redirect.
This commit is contained in:
pj committed 2026-04-26 16:17:44 +07:00
1 parent 14b4e36103
commit 7187790592
3 files changed
+47 -31

No files matched your search

@@ -2,7 +2,6 @@ package app.folio
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
@@ -17,6 +16,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import app.folio.di.AppGraph import app.folio.di.AppGraph
import app.folio.di.LocalAppGraph import app.folio.di.LocalAppGraph
import app.folio.feature.account.AddAccountRoute import app.folio.feature.account.AddAccountRoute
@@ -61,24 +66,35 @@ fun App(graphBuilder: suspend () -> AppGraph) {
@Composable @Composable
private fun AppContent() { private fun AppContent() {
val graph = LocalAppGraph.current val graph = LocalAppGraph.current
val session by graph.repository.session.collectAsState() val navController = rememberNavController()
val route by graph.navigator.current.collectAsState()
LaunchedEffect(session, route) { LaunchedEffect(navController) { graph.navigator.attach(navController) }
if (session == null && route !is Route.Login) {
val session by graph.repository.session.collectAsState()
val currentEntry by navController.currentBackStackEntryAsState()
val onLogin = currentEntry?.destination?.hasRoute(Route.Login::class) == true
LaunchedEffect(session, onLogin) {
if (session == null && !onLogin) {
graph.navigator.replace(Route.Login) graph.navigator.replace(Route.Login)
} else if (session != null && route is Route.Login) { } else if (session != null && onLogin) {
graph.navigator.replace(Route.Home) graph.navigator.replace(Route.Home)
} }
} }
Column(Modifier.fillMaxSize()) { NavHost(
when (val r = route) { navController = navController,
Route.Login -> LoginRoute() startDestination = Route.Home,
Route.Home -> HomeRoute() modifier = Modifier.fillMaxSize(),
Route.AddAccount -> AddAccountRoute() ) {
is Route.Ledger -> LedgerRoute(accountId = r.accountId) composable<Route.Login> { LoginRoute() }
is Route.AddTransaction -> AddTransactionRoute(accountId = r.accountId) composable<Route.Home> { HomeRoute() }
composable<Route.AddAccount> { AddAccountRoute() }
composable<Route.Ledger> { entry ->
LedgerRoute(accountId = entry.toRoute<Route.Ledger>().accountId)
}
composable<Route.AddTransaction> { entry ->
AddTransactionRoute(accountId = entry.toRoute<Route.AddTransaction>().accountId)
} }
} }
} }
@@ -10,7 +10,6 @@ import app.folio.feature.home.HomeViewModel
import app.folio.feature.ledger.AddTransactionViewModel import app.folio.feature.ledger.AddTransactionViewModel
import app.folio.feature.ledger.LedgerViewModel import app.folio.feature.ledger.LedgerViewModel
import app.folio.navigation.Navigator import app.folio.navigation.Navigator
import app.folio.navigation.Route
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Binds import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.DependencyGraph import dev.zacsweers.metro.DependencyGraph
@@ -33,7 +32,7 @@ interface AppGraph {
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@Provides @Provides
fun provideNavigator(): Navigator = Navigator(initial = Route.Home) fun provideNavigator(): Navigator = Navigator()
@DependencyGraph.Factory @DependencyGraph.Factory
fun interface Factory { fun interface Factory {
@@ -1,30 +1,31 @@
package app.folio.navigation package app.folio.navigation
import kotlinx.coroutines.flow.MutableStateFlow import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class Navigator(initial: Route = Route.Home) { class Navigator {
private val stack = ArrayDeque<Route>().apply { addLast(initial) } private var controller: NavHostController? = null
private val _current = MutableStateFlow<Route>(initial)
val current: StateFlow<Route> = _current.asStateFlow() fun attach(controller: NavHostController) {
this.controller = controller
}
fun push(route: Route) { fun push(route: Route) {
stack.addLast(route) controller?.navigate(route) {
_current.value = route launchSingleTop = true
}
} }
fun replace(route: Route) { fun replace(route: Route) {
stack.clear() val nav = controller ?: return
stack.addLast(route) nav.navigate(route) {
_current.value = route popUpTo(nav.graph.id) { inclusive = true }
launchSingleTop = true
}
} }
fun back(fallback: Route) { fun back(fallback: Route) {
if (stack.size > 1) { val nav = controller ?: return
stack.removeLast() if (!nav.popBackStack()) {
_current.value = stack.last()
} else {
replace(fallback) replace(fallback)
} }
} }