@@ -7,7 +7,10 @@ import androidx.compose.runtime.collectAsState
7
7
import androidx.compose.runtime.getValue
8
8
import androidx.compose.ui.Modifier
9
9
import androidx.compose.ui.window.DialogProperties
10
+ import androidx.lifecycle.Lifecycle
10
11
import androidx.lifecycle.viewmodel.compose.viewModel
12
+ import androidx.navigation.NavController
13
+ import androidx.navigation.NavGraph.Companion.findStartDestination
11
14
import androidx.navigation.NavHostController
12
15
import androidx.navigation.compose.NavHost
13
16
import androidx.navigation.compose.composable
@@ -36,26 +39,26 @@ import org.ooni.probe.ui.settings.category.SettingsCategoryScreen
36
39
import org.ooni.probe.ui.settings.proxy.ProxyScreen
37
40
import org.ooni.probe.ui.upload.UploadMeasurementsDialog
38
41
42
+ private val START_SCREEN = Screen .Dashboard
43
+
39
44
@Composable
40
45
fun Navigation (
41
46
navController : NavHostController ,
42
47
dependencies : Dependencies ,
43
48
) {
44
49
NavHost (
45
50
navController = navController,
46
- startDestination = Screen . Dashboard .route,
51
+ startDestination = START_SCREEN .route,
47
52
modifier = Modifier .fillMaxSize(),
48
53
) {
49
54
composable(route = Screen .Onboarding .route) {
50
55
val viewModel = viewModel {
51
56
dependencies.onboardingViewModel(
52
57
goToDashboard = {
53
- navController.popBackStack()
54
- navController.navigateToMainScreen(Screen .Dashboard )
58
+ navController.goBackAndNavigateToMain(Screen .Dashboard )
55
59
},
56
60
goToSettings = {
57
- navController.popBackStack()
58
- navController.navigateToMainScreen(Screen .Settings )
61
+ navController.goBackAndNavigateToMain(Screen .Settings )
59
62
},
60
63
)
61
64
}
@@ -67,17 +70,16 @@ fun Navigation(
67
70
val viewModel = viewModel {
68
71
dependencies.dashboardViewModel(
69
72
goToOnboarding = {
70
- navController.popBackStack()
71
- navController.navigate(Screen .Onboarding .route)
73
+ navController.goBackAndNavigate(Screen .Onboarding )
72
74
},
73
75
goToResults = { navController.navigateToMainScreen(Screen .Results ) },
74
- goToRunningTest = { navController.navigate (Screen .RunningTest .route ) },
75
- goToRunTests = { navController.navigate (Screen .RunTests .route ) },
76
+ goToRunningTest = { navController.safeNavigate (Screen .RunningTest ) },
77
+ goToRunTests = { navController.safeNavigate (Screen .RunTests ) },
76
78
goToDescriptor = { descriptorKey ->
77
- navController.navigate (Screen .Descriptor (descriptorKey).route )
79
+ navController.safeNavigate (Screen .Descriptor (descriptorKey))
78
80
},
79
81
goToReviewDescriptorUpdates = {
80
- navController.navigate (Screen .ReviewUpdates .route )
82
+ navController.safeNavigate (Screen .ReviewUpdates )
81
83
},
82
84
)
83
85
}
@@ -88,8 +90,8 @@ fun Navigation(
88
90
composable(route = Screen .Results .route) {
89
91
val viewModel = viewModel {
90
92
dependencies.resultsViewModel(
91
- goToResult = { navController.navigate (Screen .Result (it).route ) },
92
- goToUpload = { navController.navigate (Screen .UploadMeasurements ().route ) },
93
+ goToResult = { navController.safeNavigate (Screen .Result (it)) },
94
+ goToUpload = { navController.safeNavigate (Screen .UploadMeasurements ()) },
93
95
)
94
96
}
95
97
val state by viewModel.state.collectAsState()
@@ -100,7 +102,7 @@ fun Navigation(
100
102
val viewModel = viewModel {
101
103
dependencies.settingsViewModel(
102
104
goToSettingsForCategory = {
103
- navController.navigate (Screen .SettingsCategory (it).route )
105
+ navController.safeNavigate (Screen .SettingsCategory (it))
104
106
},
105
107
)
106
108
}
@@ -118,12 +120,12 @@ fun Navigation(
118
120
val viewModel = viewModel {
119
121
dependencies.resultViewModel(
120
122
resultId = resultId,
121
- onBack = { navController.popBackStack () },
123
+ onBack = { navController.goBack () },
122
124
goToMeasurement = { reportId, input ->
123
- navController.navigate (Screen .Measurement (reportId, input).route )
125
+ navController.safeNavigate (Screen .Measurement (reportId, input))
124
126
},
125
127
goToUpload = {
126
- navController.navigate (Screen .UploadMeasurements (resultId).route )
128
+ navController.safeNavigate (Screen .UploadMeasurements (resultId))
127
129
},
128
130
)
129
131
}
@@ -140,7 +142,7 @@ fun Navigation(
140
142
MeasurementScreen (
141
143
reportId = MeasurementModel .ReportId (reportId),
142
144
input = input,
143
- onBack = { navController.popBackStack () },
145
+ onBack = { navController.goBack () },
144
146
)
145
147
}
146
148
@@ -152,7 +154,7 @@ fun Navigation(
152
154
when (category) {
153
155
PreferenceCategoryKey .ABOUT_OONI .value -> {
154
156
val viewModel = viewModel {
155
- dependencies.aboutViewModel(onBack = { navController.navigateUp () })
157
+ dependencies.aboutViewModel(onBack = { navController.goBack () })
156
158
}
157
159
AboutScreen (
158
160
onEvent = viewModel::onEvent,
@@ -163,15 +165,15 @@ fun Navigation(
163
165
164
166
PreferenceCategoryKey .PROXY .value -> {
165
167
val viewModel = viewModel {
166
- dependencies.proxyViewModel(onBack = { navController.navigateUp () })
168
+ dependencies.proxyViewModel(onBack = { navController.goBack () })
167
169
}
168
170
val state by viewModel.state.collectAsState()
169
171
ProxyScreen (state, viewModel::onEvent)
170
172
}
171
173
172
174
PreferenceCategoryKey .SEE_RECENT_LOGS .value -> {
173
175
val viewModel = viewModel {
174
- dependencies.logViewModel(onBack = { navController.popBackStack () })
176
+ dependencies.logViewModel(onBack = { navController.goBack () })
175
177
}
176
178
val state by viewModel.state.collectAsState()
177
179
LogScreen (state, viewModel::onEvent)
@@ -182,9 +184,9 @@ fun Navigation(
182
184
dependencies.settingsCategoryViewModel(
183
185
categoryKey = category,
184
186
goToSettingsForCategory = {
185
- navController.navigate (Screen .SettingsCategory (it).route )
187
+ navController.safeNavigate (Screen .SettingsCategory (it))
186
188
},
187
- onBack = { navController.popBackStack () },
189
+ onBack = { navController.goBack () },
188
190
)
189
191
}
190
192
val state by viewModel.state.collectAsState()
@@ -195,7 +197,7 @@ fun Navigation(
195
197
196
198
composable(route = Screen .RunTests .route) {
197
199
val viewModel = viewModel {
198
- dependencies.runViewModel(onBack = { navController.popBackStack () })
200
+ dependencies.runViewModel(onBack = { navController.goBack () })
199
201
}
200
202
val state by viewModel.state.collectAsState()
201
203
RunScreen (state, viewModel::onEvent)
@@ -208,7 +210,7 @@ fun Navigation(
208
210
entry.arguments?.getLong(" runId" )?.let { descriptorId ->
209
211
val viewModel = viewModel {
210
212
dependencies.addDescriptorViewModel(
211
- onBack = { navController.popBackStack () },
213
+ onBack = { navController.goBack () },
212
214
descriptorId = descriptorId.toString(),
213
215
)
214
216
}
@@ -219,17 +221,16 @@ fun Navigation(
219
221
LaunchedEffect (Unit ) {
220
222
snackbarHostState?.showSnackbar(" Invalid descriptor ID" )
221
223
}
222
- navController.popBackStack ()
224
+ navController.goBack ()
223
225
}
224
226
}
225
227
226
228
composable(route = Screen .RunningTest .route) {
227
229
val viewModel = viewModel {
228
230
dependencies.runningViewModel(
229
- onBack = { navController.popBackStack () },
231
+ onBack = { navController.goBack () },
230
232
goToResults = {
231
- navController.popBackStack()
232
- navController.navigateToMainScreen(Screen .Results )
233
+ navController.goBackAndNavigateToMain(Screen .Results )
233
234
},
234
235
)
235
236
}
@@ -250,7 +251,7 @@ fun Navigation(
250
251
val viewModel = viewModel {
251
252
dependencies.uploadMeasurementsViewModel(
252
253
resultId = resultId,
253
- onClose = { navController.popBackStack () },
254
+ onClose = { navController.goBack () },
254
255
)
255
256
}
256
257
val state by viewModel.state.collectAsState()
@@ -265,9 +266,9 @@ fun Navigation(
265
266
val viewModel = viewModel {
266
267
dependencies.descriptorViewModel(
267
268
descriptorKey = descriptorKey,
268
- onBack = { navController.popBackStack () },
269
+ onBack = { navController.goBack () },
269
270
goToReviewDescriptorUpdates = {
270
- navController.navigate (Screen .ReviewUpdates .route )
271
+ navController.safeNavigate (Screen .ReviewUpdates )
271
272
},
272
273
goToChooseWebsites = { navController.navigate(Screen .ChooseWebsites .route) },
273
274
)
@@ -276,10 +277,10 @@ fun Navigation(
276
277
DescriptorScreen (state, viewModel::onEvent)
277
278
}
278
279
279
- composable(route = Screen .ReviewUpdates .route) { entry ->
280
+ composable(route = Screen .ReviewUpdates .route) {
280
281
val viewModel = viewModel {
281
282
dependencies.reviewUpdatesViewModel(
282
- onBack = { navController.popBackStack () },
283
+ onBack = { navController.goBack () },
283
284
)
284
285
}
285
286
val state by viewModel.state.collectAsState()
@@ -300,3 +301,64 @@ fun Navigation(
300
301
}
301
302
}
302
303
}
304
+
305
+ // Helpers
306
+
307
+ private fun NavController.goBack () {
308
+ if (! isResumed()) return
309
+ if (! popBackStack()) {
310
+ navigateToMainScreen(START_SCREEN )
311
+ }
312
+ }
313
+
314
+ private fun NavController.goBackTo (
315
+ screen : Screen ,
316
+ inclusive : Boolean = false,
317
+ ) {
318
+ if (! isResumed()) return
319
+ if (! popBackStack(screen.route, inclusive = inclusive)) {
320
+ navigateToMainScreen(START_SCREEN )
321
+ }
322
+ }
323
+
324
+ private fun NavController.goBackAndNavigate (screen : Screen ) {
325
+ if (! isResumed()) return
326
+ popBackStack()
327
+ navigate(screen.route)
328
+ }
329
+
330
+ private fun NavController.goBackAndNavigateToMain (screen : Screen ) {
331
+ if (! isResumed()) return
332
+ popBackStack()
333
+ navigateToMainScreen(screen)
334
+ }
335
+
336
+ private fun NavController.safeNavigate (screen : Screen ) {
337
+ if (! isResumed()) return
338
+ navigate(screen.route)
339
+ }
340
+
341
+ fun NavController.safeNavigateToMain (screen : Screen ) {
342
+ if (! isResumed()) return
343
+ navigateToMainScreen(screen)
344
+ }
345
+
346
+ private fun NavController.isResumed () = currentBackStackEntry?.lifecycle?.currentState == Lifecycle .State .RESUMED
347
+
348
+ private fun NavController.navigateToMainScreen (screen : Screen ) {
349
+ navigate(screen.route) {
350
+ // Pop up to the start destination of the graph to
351
+ // avoid building up a large stack of destinations
352
+ // on the back stack as users select items
353
+ graph.findStartDestination().route?.let {
354
+ popUpTo(it) {
355
+ saveState = true
356
+ }
357
+ }
358
+ // Avoid multiple copies of the same destination when
359
+ // re-selecting the same item
360
+ launchSingleTop = true
361
+ // Restore state when re-selecting a previously selected item
362
+ restoreState = true
363
+ }
364
+ }
0 commit comments