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