Skip to content

Commit 660b727

Browse files
committed
refactor: Introduce nested navigation graphs and share ViewModels
This commit refactors the navigation structure to use nested navigation graphs, improving organization and state management. It also modifies how ViewModels are scoped to share instances between screens within the same navigation graph, preserving UI state across tabs. ### Key Changes: * **Nested Navigation Graphs (`AppNavigation.kt`, `AppRoute.kt`):** * Introduced a `MainGraph` to encapsulate the primary application screens (`ProjectAnalyzer`, `StorageAnalyzer`) and a `SplashGraph` for the initial launch flow (`Splash`, `Onboarding`). * This groups related destinations, simplifying navigation logic and making the flow more robust. * **ViewModel Sharing (`ProjectAnalyzerScreen.kt`, `StorageAnalyzerScreen.kt`):** * `ProjectAnalyzerViewModel` and `StorageAnalyzerViewModel` are now scoped to the `MainGraph`. * This is achieved by passing the `NavBackStackEntry` of the parent graph (`MainGraph`) to the `koinViewModel` factory. * As a result, the ViewModels are shared across the composables within the `MainGraph`, preserving the state of each screen when switching between the "Project" and "Storage" tabs. * **Navigation Logic Cleanup (`AppNavigation.kt`):** * Removed manual `popUpTo` logic with `findStartDestination()`. The new navigation structure with `launchSingleTop = true` and `restoreState = true` handles this more cleanly. * **UI Enhancements (`TopAppBar.kt`):** * The `TopAppBar` now supports an optional back navigation icon, which can be enabled by providing a `navigationOnClick` lambda.
1 parent 40cd97a commit 660b727

File tree

8 files changed

+81
-48
lines changed

8 files changed

+81
-48
lines changed

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/components/TopAppBar.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import androidx.compose.foundation.layout.Row
44
import androidx.compose.foundation.layout.RowScope
55
import androidx.compose.foundation.layout.Spacer
66
import androidx.compose.foundation.layout.width
7+
import androidx.compose.material.icons.Icons
8+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
79
import androidx.compose.material3.CenterAlignedTopAppBar
810
import androidx.compose.material3.ExperimentalMaterial3Api
911
import androidx.compose.material3.Icon
12+
import androidx.compose.material3.IconButton
1013
import androidx.compose.material3.MaterialTheme
1114
import androidx.compose.material3.Text
1215
import androidx.compose.material3.TopAppBarDefaults
@@ -22,9 +25,20 @@ import androidx.compose.ui.unit.dp
2225
fun TopAppBar(
2326
title: String,
2427
icon: ImageVector,
28+
navigationOnClick: (() -> Unit)? = null,
2529
actions: @Composable RowScope.() -> Unit = {}
2630
) {
2731
CenterAlignedTopAppBar(
32+
navigationIcon = {
33+
navigationOnClick?.let {
34+
IconButton(onClick = it) {
35+
Icon(
36+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
37+
contentDescription = "Back"
38+
)
39+
}
40+
}
41+
},
2842
title = {
2943
Row(verticalAlignment = Alignment.CenterVertically) {
3044
Icon(
@@ -42,7 +56,8 @@ fun TopAppBar(
4256
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
4357
containerColor = MaterialTheme.colorScheme.primary,
4458
titleContentColor = MaterialTheme.colorScheme.onPrimary,
45-
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
59+
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
60+
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
4661
),
4762
actions = actions
4863
)

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation/AppNavigation.kt

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import androidx.compose.runtime.derivedStateOf
1414
import androidx.compose.runtime.getValue
1515
import androidx.compose.runtime.remember
1616
import androidx.compose.ui.Modifier
17-
import androidx.navigation.NavGraph.Companion.findStartDestination
1817
import androidx.navigation.compose.NavHost
1918
import androidx.navigation.compose.composable
2019
import androidx.navigation.compose.currentBackStackEntryAsState
20+
import androidx.navigation.compose.navigation
2121
import androidx.navigation.compose.rememberNavController
2222
import com.meet.dev.analyzer.presentation.navigation.navigation_bar.NavigationItem
2323
import com.meet.dev.analyzer.presentation.navigation.navigation_bar.NavigationRailLayout
@@ -57,29 +57,14 @@ fun AppNavigation(
5757
AnimatedVisibility(
5858
visible = currentNavigationItem != null,
5959
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
60-
enter = slideInHorizontally(
61-
// Slide in from the left
62-
initialOffsetX = { fullWidth -> -fullWidth }
63-
),
64-
exit = slideOutHorizontally(
65-
// Slide out to the right
66-
targetOffsetX = { fullWidth -> -fullWidth }
67-
),
60+
enter = slideInHorizontally { -it },
61+
exit = slideOutHorizontally { it },
6862
) {
6963
NavigationRailLayout(
7064
currentNavigationItem = currentNavigationItem,
7165
onNavigate = {
7266
navController.navigate(it.appRoute) {
73-
// Pop up to the start destination of the graph to
74-
// avoid building up a large stack of destinations
75-
// on the back stack as users select items
76-
popUpTo(navController.graph.findStartDestination().id) {
77-
saveState = true
78-
}
79-
// Avoid multiple copies of the same destination when
80-
// re-selecting the same item
8167
launchSingleTop = true
82-
// Restore state when re-selecting a previously selected item
8368
restoreState = true
8469
}
8570
},
@@ -92,36 +77,49 @@ fun AppNavigation(
9277
) {
9378
NavHost(
9479
navController = navController,
95-
startDestination = AppRoute.Splash
80+
startDestination = AppRoute.SplashGraph
9681
) {
97-
composable<AppRoute.Splash> {
98-
SplashScreen(
99-
onSplashFinished = { appRoute ->
100-
navController.navigate(appRoute) {
101-
popUpTo(AppRoute.Splash) {
102-
inclusive = true
82+
// Splash + Onboarding Graph
83+
navigation<AppRoute.SplashGraph>(
84+
startDestination = AppRoute.Splash
85+
) {
86+
composable<AppRoute.Splash> {
87+
SplashScreen(
88+
onSplashFinished = { route ->
89+
navController.navigate(route) {
90+
popUpTo(AppRoute.SplashGraph) { inclusive = true }
10391
}
10492
}
105-
}
106-
)
107-
}
108-
composable<AppRoute.Onboarding> {
109-
OnboardingScreen(
110-
onComplete = {
111-
navController.navigate(AppRoute.ProjectAnalyzer) {
112-
popUpTo(AppRoute.Onboarding) {
113-
inclusive = true
93+
)
94+
}
95+
composable<AppRoute.Onboarding> {
96+
OnboardingScreen(
97+
onComplete = {
98+
navController.navigate(AppRoute.MainGraph) {
99+
popUpTo(AppRoute.SplashGraph) { inclusive = true }
114100
}
115101
}
116-
}
117-
)
118-
}
119-
composable<AppRoute.ProjectAnalyzer> {
120-
ProjectAnalyzerScreen()
102+
)
103+
}
121104
}
122-
composable<AppRoute.StorageAnalyzer> {
123-
StorageAnalyzerScreen()
105+
106+
// Main Graph (tabs)
107+
navigation<AppRoute.MainGraph>(
108+
startDestination = AppRoute.ProjectAnalyzer
109+
) {
110+
composable<AppRoute.ProjectAnalyzer> {
111+
val parentEntry = remember(navController) {
112+
navController.getBackStackEntry(AppRoute.MainGraph)
113+
}
114+
ProjectAnalyzerScreen(parentEntry = parentEntry)
115+
}
116+
composable<AppRoute.StorageAnalyzer> {
117+
val parentEntry = remember(navController) {
118+
navController.getBackStackEntry(AppRoute.MainGraph)
119+
}
120+
StorageAnalyzerScreen(parentEntry = parentEntry)
121+
}
124122
}
125123
}
126124
}
127-
}
125+
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/navigation/AppRoute.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import kotlinx.serialization.Serializable
44

55
sealed interface AppRoute {
66

7+
@Serializable
8+
data object SplashGraph : AppRoute
79
@Serializable
810
data object Splash : AppRoute
911

1012
@Serializable
1113
data object Onboarding : AppRoute
1214

15+
@Serializable
16+
data object MainGraph : AppRoute
17+
1318
@Serializable
1419
data object ProjectAnalyzer : AppRoute
1520

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/project/ProjectAnalyzerScreen.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
2222
import androidx.compose.ui.input.pointer.PointerIcon
2323
import androidx.compose.ui.input.pointer.pointerHoverIcon
2424
import androidx.compose.ui.unit.dp
25+
import androidx.navigation.NavBackStackEntry
2526
import com.meet.dev.analyzer.core.utility.ProjectScreenTabs
2627
import com.meet.dev.analyzer.presentation.components.EmptyStateCardLayout
2728
import com.meet.dev.analyzer.presentation.components.TabLayout
@@ -43,8 +44,12 @@ import java.awt.Cursor
4344
import java.io.File
4445

4546
@Composable
46-
fun ProjectAnalyzerScreen() {
47-
val viewModel = koinViewModel<ProjectAnalyzerViewModel>()
47+
fun ProjectAnalyzerScreen(
48+
parentEntry: NavBackStackEntry
49+
) {
50+
val viewModel = koinViewModel<ProjectAnalyzerViewModel>(
51+
viewModelStoreOwner = parentEntry
52+
)
4853
val uiState by viewModel.uiState.collectAsState()
4954
val coroutineScope = rememberCoroutineScope()
5055

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/project/ProjectAnalyzerViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class ProjectAnalyzerViewModel(
2626
private val _uiState = MutableStateFlow(ProjectAnalyzerUiState())
2727
val uiState = _uiState.asStateFlow()
2828

29+
init {
30+
AppLogger.d(TAG) { "ViewModel initialized" }
31+
}
32+
2933
fun handleIntent(intent: ProjectAnalyzerIntent) {
3034
when (intent) {
3135
is ProjectAnalyzerIntent.SelectProject -> {

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/splash/SplashViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class SplashViewModel(
4444
delay(2000)
4545
val isOnboardingDone = appUiState.value
4646
val appRoute =
47-
if (isOnboardingDone) AppRoute.ProjectAnalyzer else AppRoute.Onboarding
47+
if (isOnboardingDone) AppRoute.MainGraph else AppRoute.Onboarding
4848
_effect.emit(SplashEffect.OnSplashCompleted(appRoute))
4949
}
5050
}

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/storage/StorageAnalyzerScreen.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
2727
import androidx.compose.ui.platform.ClipEntry
2828
import androidx.compose.ui.platform.LocalClipboard
2929
import androidx.compose.ui.unit.dp
30+
import androidx.navigation.NavBackStackEntry
3031
import com.meet.dev.analyzer.core.utility.StorageAnalyzerTabs
3132
import com.meet.dev.analyzer.presentation.components.ErrorLayout
3233
import com.meet.dev.analyzer.presentation.components.ProgressStatusLayout
@@ -47,8 +48,12 @@ import java.awt.Cursor
4748
import java.awt.datatransfer.StringSelection
4849

4950
@Composable
50-
fun StorageAnalyzerScreen() {
51-
val viewModel = koinViewModel<StorageAnalyzerViewModel>()
51+
fun StorageAnalyzerScreen(
52+
parentEntry: NavBackStackEntry,
53+
) {
54+
val viewModel = koinViewModel<StorageAnalyzerViewModel>(
55+
viewModelStoreOwner = parentEntry
56+
)
5257
val uiState by viewModel.uiState.collectAsState()
5358

5459
// DataClassTOJson(

composeApp/src/jvmMain/kotlin/com/meet/dev/analyzer/presentation/screen/storage/StorageAnalyzerViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class StorageAnalyzerViewModel(
4242
private var loadAllJob: Job? = null
4343

4444
init {
45+
AppLogger.d(TAG) { "ViewModel initialized" }
4546
handleIntent(StorageAnalyzerIntent.LoadAllData)
4647
}
4748

0 commit comments

Comments
 (0)