Skip to content

Commit 9a55fa1

Browse files
authored
Upgrade to Nav3 (#16)
Our Nav2 implementation had a number of small bugs, and I didn't like it. Nav 3 is a bit nicer to work with.
1 parent 4c08587 commit 9a55fa1

32 files changed

Lines changed: 431 additions & 733 deletions

app/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ dependencies {
9292
implementation(libs.androidx.ui.graphics)
9393
implementation(libs.androidx.ui.tooling.preview)
9494
implementation(libs.androidx.material3)
95-
implementation(libs.androidx.navigation.compose)
95+
implementation(libs.androidx.navigation3.runtime)
96+
implementation(libs.androidx.navigation3.ui)
97+
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
98+
implementation(libs.androidx.material3.navigation3)
9699
implementation(libs.timber)
97100
implementation(libs.kim)
98101
implementation(project.dependencies.platform(libs.koin.bom))

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@
3939

4040
<category android:name="android.intent.category.DEFAULT"/>
4141

42-
<data android:mimeType="image/jpeg"/>
42+
<data android:mimeType="image/*"/>
4343
</intent-filter>
4444
<intent-filter>
4545
<action android:name="android.intent.action.SEND_MULTIPLE"/>
4646

4747
<category android:name="android.intent.category.DEFAULT"/>
4848

49-
<data android:mimeType="image/jpeg"/>
49+
<data android:mimeType="image/*"/>
5050
</intent-filter>
5151
</activity>
5252

app/src/main/kotlin/com/darkrockstudios/app/securecamera/App.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import androidx.compose.material3.SnackbarHostState
77
import androidx.compose.runtime.*
88
import androidx.compose.ui.Modifier
99
import androidx.lifecycle.compose.LifecycleResumeEffect
10-
import androidx.navigation.NavHostController
10+
import androidx.navigation3.runtime.NavBackStack
1111
import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
1212
import com.darkrockstudios.app.securecamera.navigation.AppNavHost
13+
import com.darkrockstudios.app.securecamera.navigation.NavController
1314
import com.darkrockstudios.app.securecamera.navigation.enforceAuth
1415
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
1516
import com.darkrockstudios.app.securecamera.ui.theme.SecureCameraTheme
@@ -19,8 +20,8 @@ import org.koin.compose.koinInject
1920
@Composable
2021
fun App(
2122
capturePhoto: MutableState<Boolean?>,
22-
startDestination: String,
23-
navController: NavHostController
23+
backStack: NavBackStack,
24+
navController: NavController
2425
) {
2526
KoinContext {
2627
SecureCameraTheme {
@@ -38,11 +39,11 @@ fun App(
3839
modifier = Modifier.imePadding()
3940
) { paddingValues ->
4041
AppNavHost(
42+
backStack = backStack,
4143
navController = navController,
4244
capturePhoto = capturePhoto,
4345
modifier = Modifier,
4446
snackbarHostState = snackbarHostState,
45-
startDestination = startDestination,
4647
paddingValues = paddingValues,
4748
)
4849
}
@@ -53,14 +54,15 @@ fun App(
5354

5455
@Composable
5556
private fun VerifySessionOnResume(
56-
navController: NavHostController,
57+
navController: NavController,
5758
hasCompletedIntro: Boolean?,
5859
authorizationRepository: AuthorizationRepository
5960
) {
6061
var requireAuthCheck = remember { false }
6162
LifecycleResumeEffect(hasCompletedIntro) {
6263
if (hasCompletedIntro == true && requireAuthCheck) {
63-
enforceAuth(authorizationRepository, navController.currentDestination, navController)
64+
// Use the top-of-stack key in Nav3
65+
enforceAuth(authorizationRepository, null, navController)
6466
}
6567
onPauseOrDispose {
6668
requireAuthCheck = true

app/src/main/kotlin/com/darkrockstudios/app/securecamera/CameraVectorImages.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
55
import androidx.compose.ui.graphics.vector.path
66
import androidx.compose.ui.unit.dp
77

8-
public val Camera: ImageVector
8+
val Camera: ImageVector
99
get() {
1010
if (_Camera != null) {
1111
return _Camera!!

app/src/main/kotlin/com/darkrockstudios/app/securecamera/MainActivity.kt

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import androidx.activity.ComponentActivity
99
import androidx.activity.compose.setContent
1010
import androidx.activity.enableEdgeToEdge
1111
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.remember
1213
import androidx.lifecycle.Lifecycle
1314
import androidx.lifecycle.lifecycleScope
1415
import androidx.lifecycle.repeatOnLifecycle
15-
import androidx.navigation.NavHostController
16-
import androidx.navigation.compose.rememberNavController
16+
import androidx.navigation3.runtime.NavKey
17+
import androidx.navigation3.runtime.rememberNavBackStack
1718
import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
18-
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
19+
import com.darkrockstudios.app.securecamera.navigation.*
20+
import com.darkrockstudios.app.securecamera.navigation.Camera
1921
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
2022
import kotlinx.coroutines.delay
2123
import kotlinx.coroutines.flow.firstOrNull
@@ -31,7 +33,7 @@ class MainActivity : ComponentActivity() {
3133
private val locationRepository: LocationRepository by inject()
3234
private val preferences: AppPreferencesDataSource by inject()
3335
private val authorizationRepository: AuthorizationRepository by inject()
34-
lateinit var navController: NavHostController
36+
lateinit var navController: NavController
3537

3638
override fun onCreate(savedInstanceState: Bundle?) {
3739
super.onCreate(savedInstanceState)
@@ -45,33 +47,34 @@ class MainActivity : ComponentActivity() {
4547

4648
enableEdgeToEdge()
4749

48-
val startDestination = determineStartRoute()
50+
val startKey = determineStartKey()
4951
setContent {
50-
navController = rememberNavController()
51-
App(capturePhoto, startDestination, navController)
52+
val backStack = rememberNavBackStack(startKey)
53+
val controller = remember(backStack) { Nav3CompatController(backStack) }
54+
navController = controller
55+
App(capturePhoto, backStack, navController)
5256
}
5357

5458
startKeepAliveWatcher()
5559
}
5660

57-
private fun determineStartRoute(): String {
61+
private fun determineStartKey(): NavKey {
5862
val photosToImport = receiveFiles()
5963
val hasCompletedIntro = runBlocking { preferences.hasCompletedIntro.firstOrNull() ?: false }
60-
val startDestination = if (hasCompletedIntro) {
61-
val targetDestination = if (photosToImport.isNotEmpty()) {
62-
AppDestinations.createImportPhotosRoute(photosToImport)
64+
return if (hasCompletedIntro) {
65+
val targetKey: DestinationKey = if (photosToImport.isNotEmpty()) {
66+
ImportPhotos(PhotoImportJob(photosToImport))
6367
} else {
64-
AppDestinations.CAMERA_ROUTE
68+
Camera
6569
}
6670
if (authorizationRepository.checkSessionValidity()) {
67-
targetDestination
71+
targetKey
6872
} else {
69-
AppDestinations.createPinVerificationRoute(targetDestination)
73+
PinVerification(targetKey)
7074
}
7175
} else {
72-
AppDestinations.INTRODUCTION_ROUTE
76+
Introduction
7377
}
74-
return startDestination
7578
}
7679

7780
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -117,13 +120,13 @@ class MainActivity : ComponentActivity() {
117120
val intent = getIntent()
118121

119122
return if (Intent.ACTION_SEND == intent.action && intent.type != null) {
120-
if (intent.type?.startsWith("image/jpeg") == true) {
123+
if (intent.type?.startsWith("image/") == true) {
121124
handleSingleImage(intent)
122125
} else {
123126
emptyList()
124127
}
125128
} else if (Intent.ACTION_SEND_MULTIPLE == intent.action && intent.type != null) {
126-
if (intent.type?.startsWith("image/jpeg") == true) {
129+
if (intent.type?.startsWith("image/") == true) {
127130
handleMultipleImages(intent)
128131
} else {
129132
emptyList()

app/src/main/kotlin/com/darkrockstudios/app/securecamera/about/AboutContent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ import androidx.compose.ui.res.stringResource
1717
import androidx.compose.ui.text.style.TextDecoration
1818
import androidx.compose.ui.unit.dp
1919
import androidx.core.net.toUri
20-
import androidx.navigation.NavHostController
2120
import com.darkrockstudios.app.securecamera.R
21+
import com.darkrockstudios.app.securecamera.navigation.NavController
2222

2323
/**
2424
* About screen content
2525
*/
2626
@OptIn(ExperimentalMaterial3Api::class)
2727
@Composable
2828
fun AboutContent(
29-
navController: NavHostController,
29+
navController: NavController,
3030
modifier: Modifier = Modifier,
3131
paddingValues: PaddingValues,
3232
) {

app/src/main/kotlin/com/darkrockstudios/app/securecamera/auth/PinVerificationContent.kt

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
11
package com.darkrockstudios.app.securecamera.auth
22

33
import androidx.compose.foundation.background
4-
import androidx.compose.foundation.layout.Arrangement
5-
import androidx.compose.foundation.layout.Box
6-
import androidx.compose.foundation.layout.Column
7-
import androidx.compose.foundation.layout.Spacer
8-
import androidx.compose.foundation.layout.fillMaxSize
9-
import androidx.compose.foundation.layout.fillMaxWidth
10-
import androidx.compose.foundation.layout.height
11-
import androidx.compose.foundation.layout.padding
12-
import androidx.compose.foundation.layout.size
13-
import androidx.compose.foundation.layout.widthIn
4+
import androidx.compose.foundation.layout.*
145
import androidx.compose.foundation.text.KeyboardActions
156
import androidx.compose.foundation.text.KeyboardOptions
167
import androidx.compose.material.icons.Icons
178
import androidx.compose.material.icons.filled.Camera
18-
import androidx.compose.material3.Button
19-
import androidx.compose.material3.ButtonDefaults
20-
import androidx.compose.material3.CircularProgressIndicator
21-
import androidx.compose.material3.Icon
22-
import androidx.compose.material3.MaterialTheme
23-
import androidx.compose.material3.OutlinedTextField
24-
import androidx.compose.material3.SnackbarHostState
25-
import androidx.compose.material3.Text
26-
import androidx.compose.runtime.Composable
27-
import androidx.compose.runtime.LaunchedEffect
28-
import androidx.compose.runtime.getValue
29-
import androidx.compose.runtime.mutableStateOf
30-
import androidx.compose.runtime.remember
9+
import androidx.compose.material3.*
10+
import androidx.compose.runtime.*
3111
import androidx.compose.runtime.saveable.rememberSaveable
32-
import androidx.compose.runtime.setValue
3312
import androidx.compose.ui.Alignment
3413
import androidx.compose.ui.Modifier
3514
import androidx.compose.ui.focus.FocusRequester
@@ -41,8 +20,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
4120
import androidx.compose.ui.text.style.TextAlign
4221
import androidx.compose.ui.unit.dp
4322
import androidx.lifecycle.compose.collectAsStateWithLifecycle
44-
import androidx.navigation.NavController
23+
import androidx.navigation3.runtime.NavKey
4524
import com.darkrockstudios.app.securecamera.R
25+
import com.darkrockstudios.app.securecamera.navigation.NavController
26+
import com.darkrockstudios.app.securecamera.navigation.navigateClearingBackStack
4627
import com.darkrockstudios.app.securecamera.ui.HandleUiEvents
4728
import org.koin.androidx.compose.koinViewModel
4829

@@ -53,7 +34,7 @@ import org.koin.androidx.compose.koinViewModel
5334
fun PinVerificationContent(
5435
navController: NavController,
5536
snackbarHostState: SnackbarHostState,
56-
returnRoute: String,
37+
returnKey: NavKey,
5738
modifier: Modifier = Modifier
5839
) {
5940
val viewModel: PinVerificationViewModel = koinViewModel()
@@ -117,11 +98,9 @@ fun PinVerificationContent(
11798
fun verifyPin() {
11899
viewModel.verify(
119100
pin = pin,
120-
returnRoute = returnRoute,
121-
onNavigate = {
122-
navController.navigate(it) {
123-
popUpTo(0) { inclusive = true }
124-
}
101+
returnKey = returnKey,
102+
onNavigate = { destKey ->
103+
navController.navigateClearingBackStack(destKey)
125104
},
126105
onFailure = { pin = "" }
127106
)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/auth/PinVerificationViewModel.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package com.darkrockstudios.app.securecamera.auth
22

33
import android.content.Context
44
import androidx.lifecycle.viewModelScope
5+
import androidx.navigation3.runtime.NavKey
56
import com.darkrockstudios.app.securecamera.BaseViewModel
67
import com.darkrockstudios.app.securecamera.R
78
import com.darkrockstudios.app.securecamera.gallery.vibrateDevice
8-
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
9+
import com.darkrockstudios.app.securecamera.navigation.Introduction
910
import com.darkrockstudios.app.securecamera.usecases.InvalidateSessionUseCase
1011
import com.darkrockstudios.app.securecamera.usecases.PinSizeUseCase
1112
import com.darkrockstudios.app.securecamera.usecases.SecurityResetUseCase
@@ -90,7 +91,8 @@ class PinVerificationViewModel(
9091
}
9192
}
9293

93-
fun verify(pin: String, returnRoute: String, onNavigate: (String) -> Unit, onFailure: () -> Unit) {
94+
95+
fun verify(pin: String, returnKey: NavKey, onNavigate: (NavKey) -> Unit, onFailure: () -> Unit) {
9496
val currentState = uiState.value
9597

9698
if (pin.isBlank()) {
@@ -115,8 +117,7 @@ class PinVerificationViewModel(
115117
failedAttempts = 0
116118
)
117119
}
118-
119-
onNavigate(returnRoute)
120+
onNavigate(returnKey)
120121
}
121122
} else {
122123
val newFailedAttempts = authRepository.incrementFailedAttempts()
@@ -144,7 +145,7 @@ class PinVerificationViewModel(
144145
// Nuke it all
145146
securityResetUseCase.reset()
146147
showMessage(appContext.getString(R.string.pin_verification_all_data_deleted))
147-
onNavigate(AppDestinations.INTRODUCTION_ROUTE)
148+
onNavigate(Introduction)
148149
}
149150

150151
onFailure()

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/BottomCameraControls.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ import androidx.compose.ui.res.stringResource
1919
import androidx.compose.ui.semantics.contentDescription
2020
import androidx.compose.ui.semantics.semantics
2121
import androidx.compose.ui.unit.dp
22-
import androidx.navigation.NavHostController
2322
import com.darkrockstudios.app.securecamera.R
24-
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
23+
import com.darkrockstudios.app.securecamera.navigation.Gallery
24+
import com.darkrockstudios.app.securecamera.navigation.NavController
25+
import com.darkrockstudios.app.securecamera.navigation.Settings
2526

2627
@Composable
2728
fun BottomCameraControls(
2829
modifier: Modifier = Modifier,
2930
onCapture: (() -> Unit)?,
3031
isLoading: Boolean,
31-
navController: NavHostController,
32+
navController: NavController,
3233
) {
3334
val context = LocalContext.current
3435

@@ -38,7 +39,7 @@ fun BottomCameraControls(
3839
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
3940
) {
4041
ElevatedButton(
41-
onClick = { navController.navigate(AppDestinations.SETTINGS_ROUTE) },
42+
onClick = { navController.navigate(Settings) },
4243
enabled = isLoading.not(),
4344
modifier = Modifier.align(Alignment.BottomStart),
4445
) {
@@ -73,7 +74,7 @@ fun BottomCameraControls(
7374
}
7475

7576
ElevatedButton(
76-
onClick = { navController.navigate(AppDestinations.GALLERY_ROUTE) },
77+
onClick = { navController.navigate(Gallery) },
7778
enabled = isLoading.not(),
7879
modifier = Modifier.align(Alignment.BottomEnd),
7980
) {

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraContent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import androidx.compose.foundation.layout.PaddingValues
66
import androidx.compose.foundation.layout.fillMaxSize
77
import androidx.compose.runtime.*
88
import androidx.compose.ui.Modifier
9-
import androidx.navigation.NavHostController
109
import com.darkrockstudios.app.securecamera.KeepScreenOnEffect
10+
import com.darkrockstudios.app.securecamera.navigation.NavController
1111
import com.google.accompanist.permissions.ExperimentalPermissionsApi
1212
import com.google.accompanist.permissions.rememberMultiplePermissionsState
1313

1414
@OptIn(ExperimentalPermissionsApi::class)
1515
@Composable
1616
internal fun CameraContent(
1717
capturePhoto: MutableState<Boolean?>,
18-
navController: NavHostController,
18+
navController: NavController,
1919
modifier: Modifier,
2020
paddingValues: PaddingValues,
2121
) {

0 commit comments

Comments
 (0)