From 1412bc20ea184f4da82cf8c373089f1e9cd6cc56 Mon Sep 17 00:00:00 2001 From: "babichev.a" Date: Wed, 23 Oct 2024 20:01:02 +0400 Subject: [PATCH] type-safe navigation --- .../notedelight/ui/DesktopUiTests.kt | 2 +- gradle/libs.versions.toml | 4 +- iosApp/Podfile.lock | 2 +- iosApp/Pods/Manifest.lock | 2 +- iosApp/Pods/Pods.xcodeproj/project.pbxproj | 4 +- .../Pods-iosApp/Pods-iosApp-frameworks.sh | 2 +- iosApp/iosApp/Info.plist | 2 + .../softartdev/notedelight/UiThreadRouter.kt | 6 +- shared-compose-ui/build.gradle.kts | 1 - .../kotlin/com/softartdev/notedelight/App.kt | 49 +++++------- .../notedelight/navigation/RouterImpl.kt | 6 +- shared/build.gradle.kts | 2 + .../presentation/main/MainViewModelTest.kt | 6 +- .../presentation/note/NoteViewModelTest.kt | 4 +- .../settings/SettingsViewModelTest.kt | 10 +-- .../signin/SignInViewModelTest.kt | 4 +- .../splash/SplashViewModelTest.kt | 6 +- .../shared/navigation/AppNavGraph.kt | 77 +++++++++++-------- .../notedelight/shared/navigation/Router.kt | 6 +- .../shared/presentation/main/MainViewModel.kt | 6 +- .../shared/presentation/note/NoteViewModel.kt | 10 +-- .../settings/SettingsViewModel.kt | 18 ++--- .../presentation/signin/SignInViewModel.kt | 4 +- .../presentation/splash/SplashViewModel.kt | 6 +- .../shared/navigation/RouterStub.kt | 6 +- 25 files changed, 123 insertions(+), 122 deletions(-) diff --git a/desktop-compose-app/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt b/desktop-compose-app/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt index a770af25..ef204412 100644 --- a/desktop-compose-app/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt +++ b/desktop-compose-app/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt @@ -5,13 +5,13 @@ package com.softartdev.notedelight.ui import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick +import androidx.lifecycle.compose.LocalLifecycleOwner import com.softartdev.notedelight.App import com.softartdev.notedelight.TestLifecycleOwner import com.softartdev.notedelight.di.uiTestModules diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d5874da..0977a75f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,11 +15,12 @@ saferoom = "1.3.0" androidSqlCipher = "4.5.4" iosSqlCipher = "4.5.4" koin = "3.5.6" +kotlinx-serialization = "1.7.3" kotlinx-datetime = "0.6.0" napier = "2.7.1" materialThemePrefs = "0.9.0" androidxViewModel = "2.8.3" -androidxNavigation = "2.7.0-alpha07" +androidxNavigation = "2.8.0-alpha10" androidxActivityCompose = "1.9.3" androidxComposeTest = "1.7.4" androidxCoreSplashscreen = "1.0.1" @@ -66,6 +67,7 @@ koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", versi koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } napier = { module = "io.github.aakira:napier", version.ref = "napier" } diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock index bf773ca3..641ccb94 100644 --- a/iosApp/Podfile.lock +++ b/iosApp/Podfile.lock @@ -24,4 +24,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 0dc93a6f6109335ea8cd3f91d2c87cc8c99f04a3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/iosApp/Pods/Manifest.lock b/iosApp/Pods/Manifest.lock index bf773ca3..641ccb94 100644 --- a/iosApp/Pods/Manifest.lock +++ b/iosApp/Pods/Manifest.lock @@ -24,4 +24,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 0dc93a6f6109335ea8cd3f91d2c87cc8c99f04a3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/iosApp/Pods/Pods.xcodeproj/project.pbxproj b/iosApp/Pods/Pods.xcodeproj/project.pbxproj index 4269bd41..816688e6 100644 --- a/iosApp/Pods/Pods.xcodeproj/project.pbxproj +++ b/iosApp/Pods/Pods.xcodeproj/project.pbxproj @@ -328,8 +328,8 @@ BFDFE7DC352907FC980B868725387E98 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1300; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; }; buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; compatibilityVersion = "Xcode 12.0"; diff --git a/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh b/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh index 94102de6..79bf656a 100755 --- a/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh +++ b/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh @@ -18,7 +18,7 @@ echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" -SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" BCSYMBOLMAP_DIR="BCSymbolMaps" diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index af6afcdd..312a3601 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -51,5 +51,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + CADisableMinimumFrameDurationOnPhone + diff --git a/jvm-compose-test/src/main/kotlin/com/softartdev/notedelight/UiThreadRouter.kt b/jvm-compose-test/src/main/kotlin/com/softartdev/notedelight/UiThreadRouter.kt index 70dae606..7ddc691e 100644 --- a/jvm-compose-test/src/main/kotlin/com/softartdev/notedelight/UiThreadRouter.kt +++ b/jvm-compose-test/src/main/kotlin/com/softartdev/notedelight/UiThreadRouter.kt @@ -11,13 +11,13 @@ class UiThreadRouter(private val router: Router) : Router { override fun releaseController() = runOnUiThread { router.releaseController() } - override fun navigate(route: String) = runOnUiThread { router.navigate(route) } + override fun navigate(route: T) = runOnUiThread { router.navigate(route) } - override fun navigateClearingBackStack(route: String) = runOnUiThread { + override fun navigateClearingBackStack(route: T) = runOnUiThread { router.navigateClearingBackStack(route) } - override fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean = + override fun popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean = runOnUiThread { router.popBackStack(route, inclusive, saveState) } override fun popBackStack(): Boolean = runOnUiThread { router.popBackStack() } diff --git a/shared-compose-ui/build.gradle.kts b/shared-compose-ui/build.gradle.kts index 48758752..0dcec11e 100644 --- a/shared-compose-ui/build.gradle.kts +++ b/shared-compose-ui/build.gradle.kts @@ -8,7 +8,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.serialization) } compose.resources { publicResClass = true diff --git a/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/App.kt b/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/App.kt index 0d21ecc5..41555d13 100644 --- a/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/App.kt +++ b/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/App.kt @@ -4,12 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.softartdev.notedelight.di.getViewModel import com.softartdev.notedelight.shared.navigation.AppNavGraph import com.softartdev.notedelight.shared.navigation.Router @@ -40,30 +39,27 @@ fun App( } NavHost( navController = navController, - startDestination = AppNavGraph.Splash.route, + startDestination = AppNavGraph.Splash, ) { - composable(route = AppNavGraph.Splash.route) { + composable { SplashScreen(splashViewModel = getViewModel()) } - composable(route = AppNavGraph.SignIn.route) { + composable { SignInScreen(signInViewModel = getViewModel()) } - composable(route = AppNavGraph.Main.route) { + composable { MainScreen(mainViewModel = getViewModel()) } - composable( - route = AppNavGraph.Details.route, - arguments = listOf(navArgument(name = AppNavGraph.ARG_NOTE_ID) { type = NavType.LongType }) - ) { backStackEntry: NavBackStackEntry -> + composable { backStackEntry: NavBackStackEntry -> NoteDetail( noteViewModel = getViewModel(), - noteId = AppNavGraph.ARG_NOTE_ID.let(backStackEntry.arguments!!::getLong), + noteId = backStackEntry.toRoute().noteId, ) } - composable(route = AppNavGraph.Settings.route) { + composable { SettingsScreen(settingsViewModel = getViewModel()) } - dialog(route = AppNavGraph.ThemeDialog.route) { + dialog { val preferenceHelper: PreferenceHelper = themePrefs.preferenceHelper ThemeDialog( darkThemeState = themePrefs.darkThemeState, @@ -71,39 +67,30 @@ fun App( dismissDialog = navController::navigateUp, ) } - dialog(route = AppNavGraph.SaveChangesDialog.route) { + dialog { SaveDialog(saveViewModel = getViewModel()) } - dialog( - route = AppNavGraph.EditTitleDialog.route, - arguments = listOf(navArgument(name = AppNavGraph.ARG_NOTE_ID) { type = NavType.LongType }) - ) { backStackEntry: NavBackStackEntry -> + dialog { backStackEntry: NavBackStackEntry -> EditTitleDialog( - noteId = AppNavGraph.ARG_NOTE_ID.let(backStackEntry.arguments!!::getLong), + noteId = backStackEntry.toRoute().noteId, editTitleViewModel = getViewModel() ) } - dialog(route = AppNavGraph.DeleteNoteDialog.route) { + dialog { DeleteDialog(deleteViewModel = getViewModel()) } - dialog(route = AppNavGraph.EnterPasswordDialog.route) { + dialog { EnterPasswordDialog(enterViewModel = getViewModel()) } - dialog(route = AppNavGraph.ConfirmPasswordDialog.route) { + dialog { ConfirmPasswordDialog(confirmViewModel = getViewModel()) } - dialog(route = AppNavGraph.ChangePasswordDialog.route) { + dialog { ChangePasswordDialog(changeViewModel = getViewModel()) } - dialog( - route = AppNavGraph.ErrorDialog.route, - arguments = listOf(navArgument(name = AppNavGraph.ARG_MESSAGE) { - type = NavType.StringType - nullable = true - }) - ) { backStackEntry: NavBackStackEntry -> + dialog { backStackEntry: NavBackStackEntry -> ErrorDialog( - message = AppNavGraph.ARG_MESSAGE.let(backStackEntry.arguments!!::getString), + message = backStackEntry.toRoute().message, dismissDialog = navController::navigateUp ) } diff --git a/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/navigation/RouterImpl.kt b/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/navigation/RouterImpl.kt index 48f7f14f..94942c83 100644 --- a/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/navigation/RouterImpl.kt +++ b/shared-compose-ui/src/commonMain/kotlin/com/softartdev/notedelight/navigation/RouterImpl.kt @@ -15,9 +15,9 @@ class RouterImpl : Router { navController = null } - override fun navigate(route: String) = navController!!.navigate(route) + override fun navigate(route: T) = navController!!.navigate(route) - override fun navigateClearingBackStack(route: String) { + override fun navigateClearingBackStack(route: T) { var popped = true while (popped) { popped = navController!!.popBackStack() @@ -25,7 +25,7 @@ class RouterImpl : Router { navController!!.navigate(route) } - override fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean = + override fun popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean = navController!!.popBackStack(route, inclusive, saveState) override fun popBackStack() = navController!!.popBackStack() diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b5829fb8..c26efc6b 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.gradle.convention) alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.cocoapods) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.sqlDelight) alias(libs.plugins.android.library) } @@ -65,6 +66,7 @@ kotlin { implementation(libs.koin.core) api(libs.material.theme.prefs) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.kotlinx.serialization.json) } commonTest.dependencies { implementation(kotlin("test")) diff --git a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModelTest.kt b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModelTest.kt index 7ec657cb..df0644a3 100644 --- a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModelTest.kt @@ -63,7 +63,7 @@ class MainViewModelTest { Mockito.`when`(mockNoteDAO.listFlow).thenReturn(flow { throw SQLiteException() }) mainViewModel.updateNotes() assertEquals(NoteListResult.Error(null), awaitItem()) - Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.SignIn.name) + Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.SignIn) cancelAndIgnoreRemainingEvents() } @@ -72,13 +72,13 @@ class MainViewModelTest { @Test fun onNoteClicked() { mainViewModel.onNoteClicked(1) - Mockito.verify(mockRouter).navigate(route = "${AppNavGraph.Details.name}/1") + Mockito.verify(mockRouter).navigate(route = AppNavGraph.Details(noteId = 1)) } @Test fun onSettingsClicked() { mainViewModel.onSettingsClicked() - Mockito.verify(mockRouter).navigate(route = AppNavGraph.Settings.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.Settings) } @Test diff --git a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModelTest.kt b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModelTest.kt index ac70d527..8a81a1db 100644 --- a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModelTest.kt @@ -118,7 +118,7 @@ class NoteViewModelTest { noteViewModel.setIdForTest(id) noteViewModel.editTitle() - Mockito.verify(mockRouter).navigate(route = "${AppNavGraph.EditTitleDialog.name}/$id") + Mockito.verify(mockRouter).navigate(route = AppNavGraph.EditTitleDialog(noteId = id)) UpdateTitleUseCase.titleChannel.send(title) assertEquals(NoteResult.TitleUpdated(title), awaitItem()) @@ -148,7 +148,7 @@ class NoteViewModelTest { noteViewModel.setIdForTest(id) Mockito.`when`(mockNoteDAO.load(id)).thenReturn(note.copy(text = "new text")) noteViewModel.checkSaveChange(title, text) - Mockito.verify(mockRouter).navigate(route = AppNavGraph.SaveChangesDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.SaveChangesDialog) SaveNoteUseCase.dialogChannel.send(true) Mockito.verify(mockRouter).popBackStack() diff --git a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModelTest.kt b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModelTest.kt index 00d264d5..dedb9528 100644 --- a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModelTest.kt @@ -35,7 +35,7 @@ class SettingsViewModelTest { @Test fun changeTheme() = runTest { settingsViewModel.stateFlow.value.changeTheme.invoke() - Mockito.verify(mockRouter).navigate(route = AppNavGraph.ThemeDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.ThemeDialog) Mockito.verifyNoMoreInteractions(mockRouter) } @@ -65,7 +65,7 @@ class SettingsViewModelTest { settingsViewModel.stateFlow.test { assertFalse(awaitItem().loading) settingsViewModel.stateFlow.value.changeEncryption.invoke(true) - Mockito.verify(mockRouter).navigate(route = AppNavGraph.ConfirmPasswordDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.ConfirmPasswordDialog) expectNoEvents() } Mockito.verifyNoMoreInteractions(mockRouter) @@ -77,7 +77,7 @@ class SettingsViewModelTest { settingsViewModel.stateFlow.test { assertFalse(awaitItem().loading) settingsViewModel.stateFlow.value.changeEncryption.invoke(false) - Mockito.verify(mockRouter).navigate(route = AppNavGraph.EnterPasswordDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.EnterPasswordDialog) expectNoEvents() } Mockito.verifyNoMoreInteractions(mockRouter) @@ -102,7 +102,7 @@ class SettingsViewModelTest { settingsViewModel.stateFlow.test { assertFalse(awaitItem().loading) settingsViewModel.stateFlow.value.changePassword.invoke() - Mockito.verify(mockRouter).navigate(route = AppNavGraph.ChangePasswordDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.ChangePasswordDialog) expectNoEvents() } Mockito.verifyNoMoreInteractions(mockRouter) @@ -114,7 +114,7 @@ class SettingsViewModelTest { settingsViewModel.stateFlow.test { assertFalse(awaitItem().loading) settingsViewModel.stateFlow.value.changePassword.invoke() - Mockito.verify(mockRouter).navigate(route = AppNavGraph.ConfirmPasswordDialog.name) + Mockito.verify(mockRouter).navigate(route = AppNavGraph.ConfirmPasswordDialog) expectNoEvents() } Mockito.verifyNoMoreInteractions(mockRouter) diff --git a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModelTest.kt b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModelTest.kt index 80784b57..27c81a2c 100644 --- a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModelTest.kt @@ -51,7 +51,7 @@ class SignInViewModelTest { val pass = StubEditable("pass") Mockito.`when`(mockCheckPasswordUseCase(pass)).thenReturn(true) signInViewModel.signIn(pass) - Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.Main.name) + Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.Main) cancelAndIgnoreRemainingEvents() } @@ -92,7 +92,7 @@ class SignInViewModelTest { Mockito.`when`(mockCheckPasswordUseCase(anyObject())).thenThrow(throwable) signInViewModel.signIn(StubEditable("pass")) Mockito.verify(mockRouter).navigate( - route = AppNavGraph.ErrorDialog.argRoute(message = throwable.message) + route = AppNavGraph.ErrorDialog(message = throwable.message) ) cancelAndIgnoreRemainingEvents() } diff --git a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModelTest.kt b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModelTest.kt index 70ed8da3..a49648b0 100644 --- a/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModelTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModelTest.kt @@ -35,7 +35,7 @@ class SplashViewModelTest { splashViewModel.checkEncryption() - Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.SignIn.name) + Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.SignIn) assertEquals(false, awaitItem()) cancelAndIgnoreRemainingEvents() @@ -51,7 +51,7 @@ class SplashViewModelTest { splashViewModel.checkEncryption() - Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.Main.name) + Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.Main) assertEquals(false, awaitItem()) cancelAndIgnoreRemainingEvents() @@ -68,7 +68,7 @@ class SplashViewModelTest { splashViewModel.checkEncryption() Mockito.verify(mockRouter).navigate( - route = AppNavGraph.ErrorDialog.argRoute(message = null) + route = AppNavGraph.ErrorDialog(message = null) ) assertEquals(false, awaitItem()) diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/AppNavGraph.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/AppNavGraph.kt index 31a92c2b..7613681d 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/AppNavGraph.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/AppNavGraph.kt @@ -1,36 +1,45 @@ package com.softartdev.notedelight.shared.navigation -enum class AppNavGraph { - Splash, - SignIn, - Main, - Details, - Settings, - ThemeDialog, - SaveChangesDialog, - EditTitleDialog, - DeleteNoteDialog, - EnterPasswordDialog, - ConfirmPasswordDialog, - ChangePasswordDialog, - ErrorDialog; - - val route: String - get() = when (this) { - Details, EditTitleDialog -> "$name/{$ARG_NOTE_ID}" - ErrorDialog -> "$name?$ARG_MESSAGE={$ARG_MESSAGE}" - else -> name - } - - fun argRoute(message: String?): String = when (message) { - null -> name - else -> "$name?$ARG_MESSAGE=$message" - } - - fun argRoute(noteId: Long): String = "$name/$noteId" - - companion object { - const val ARG_NOTE_ID = "noteId" - const val ARG_MESSAGE = "message" - } -} \ No newline at end of file +import kotlinx.serialization.Serializable + +sealed interface AppNavGraph { + + @Serializable + data object Splash : AppNavGraph + + @Serializable + data object SignIn : AppNavGraph + + @Serializable + data object Main : AppNavGraph + + @Serializable + data class Details(val noteId: Long) : AppNavGraph + + @Serializable + data object Settings : AppNavGraph + + @Serializable + data object ThemeDialog : AppNavGraph + + @Serializable + data object SaveChangesDialog : AppNavGraph + + @Serializable + data class EditTitleDialog(val noteId: Long) : AppNavGraph + + @Serializable + data object DeleteNoteDialog : AppNavGraph + + @Serializable + data object EnterPasswordDialog : AppNavGraph + + @Serializable + data object ConfirmPasswordDialog : AppNavGraph + + @Serializable + data object ChangePasswordDialog : AppNavGraph + + @Serializable + data class ErrorDialog(val message: String?) : AppNavGraph +} diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/Router.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/Router.kt index 47d4cfb5..c2df26fb 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/Router.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/navigation/Router.kt @@ -6,11 +6,11 @@ interface Router { fun releaseController() - fun navigate(route: String) + fun navigate(route: T) - fun navigateClearingBackStack(route: String) + fun navigateClearingBackStack(route: T) - fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean + fun popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean fun popBackStack(): Boolean } diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModel.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModel.kt index 209161fe..cd7fb53c 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModel.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/main/MainViewModel.kt @@ -41,12 +41,12 @@ class MainViewModel( Napier.e("❌", throwable) mutableStateFlow.value = NoteListResult.Error(throwable.message) if (throwable::class.simpleName.orEmpty().contains("SQLite")) { - router.navigateClearingBackStack(AppNavGraph.SignIn.name) + router.navigateClearingBackStack(AppNavGraph.SignIn) } }.launchIn(this) } - fun onNoteClicked(id: Long) = router.navigate(route = AppNavGraph.Details.argRoute(noteId = id)) + fun onNoteClicked(id: Long) = router.navigate(route = AppNavGraph.Details(noteId = id)) - fun onSettingsClicked() = router.navigate(route = AppNavGraph.Settings.name) + fun onSettingsClicked() = router.navigate(route = AppNavGraph.Settings) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModel.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModel.kt index faac8096..4ad76a01 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModel.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/note/NoteViewModel.kt @@ -70,7 +70,7 @@ class NoteViewModel( fun editTitle() = viewModelScope.launch { subscribeToEditTitle() - router.navigate(route = AppNavGraph.EditTitleDialog.argRoute(noteId = noteId)) + router.navigate(route = AppNavGraph.EditTitleDialog(noteId = noteId)) } fun deleteNote() = viewModelScope.launch { @@ -83,7 +83,7 @@ class NoteViewModel( val empty: Boolean = isEmpty(noteId) when { changed -> { - router.navigate(route = AppNavGraph.SaveChangesDialog.name) + router.navigate(route = AppNavGraph.SaveChangesDialog) subscribeToSaveNote(title, text) } empty -> mutableStateFlow.value = deleteNoteForResult() @@ -120,7 +120,7 @@ class NoteViewModel( } fun subscribeToDeleteNote() = viewModelScope.launch { - router.navigate(route = AppNavGraph.DeleteNoteDialog.name) + router.navigate(route = AppNavGraph.DeleteNoteDialog) val doDelete: Boolean = withContext(coroutineDispatchers.io) { DeleteNoteUseCase.deleteChannel.receive() } @@ -137,7 +137,7 @@ class NoteViewModel( deleteNoteUseCase.invoke(id = noteId) } Napier.d("Deleted note with id=$noteId") - router.popBackStack(route = AppNavGraph.Main.name, inclusive = false, saveState = false) + router.popBackStack(route = AppNavGraph.Main, inclusive = false, saveState = false) return NoteResult.Deleted } @@ -150,7 +150,7 @@ class NoteViewModel( mutableStateFlow.value = NoteResult.TitleUpdated(title) } catch (e: Throwable) { Napier.e("❌", e) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = e.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = e.message)) } } diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModel.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModel.kt index 9800115f..6ebb8bb8 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/settings/SettingsViewModel.kt @@ -34,7 +34,7 @@ class SettingsViewModel( private val dbIsEncrypted: Boolean get() = safeRepo.databaseState == PlatformSQLiteState.ENCRYPTED - private fun changeTheme() = router.navigate(route = AppNavGraph.ThemeDialog.name) + private fun changeTheme() = router.navigate(route = AppNavGraph.ThemeDialog) private fun checkEncryption() = viewModelScope.launch { mutableStateFlow.update(SecurityResult::showLoading) @@ -42,7 +42,7 @@ class SettingsViewModel( mutableStateFlow.update { result -> result.copy(encryption = dbIsEncrypted) } } catch (e: Throwable) { Napier.e("❌", e) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = e.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = e.message)) } finally { mutableStateFlow.update(SecurityResult::hideLoading) } @@ -52,15 +52,15 @@ class SettingsViewModel( mutableStateFlow.update(SecurityResult::showLoading) try { when { - checked -> router.navigate(route = AppNavGraph.ConfirmPasswordDialog.name) + checked -> router.navigate(route = AppNavGraph.ConfirmPasswordDialog) else -> when { - dbIsEncrypted -> router.navigate(route = AppNavGraph.EnterPasswordDialog.name) + dbIsEncrypted -> router.navigate(route = AppNavGraph.EnterPasswordDialog) else -> mutableStateFlow.update(SecurityResult::hideEncryption) } } } catch (e: Throwable) { Napier.e("❌", e) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = e.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = e.message)) } finally { mutableStateFlow.update(SecurityResult::hideLoading) } @@ -70,12 +70,12 @@ class SettingsViewModel( mutableStateFlow.update(SecurityResult::showLoading) try { when { - dbIsEncrypted -> router.navigate(route = AppNavGraph.ChangePasswordDialog.name) - else -> router.navigate(route = AppNavGraph.ConfirmPasswordDialog.name) + dbIsEncrypted -> router.navigate(route = AppNavGraph.ChangePasswordDialog) + else -> router.navigate(route = AppNavGraph.ConfirmPasswordDialog) } } catch (e: Throwable) { Napier.e("❌", e) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = e.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = e.message)) } finally { mutableStateFlow.update(SecurityResult::hideLoading) } @@ -88,7 +88,7 @@ class SettingsViewModel( mutableStateFlow.update { result -> result.copy(snackBarMessage = cipherVersion) } } catch (e: Throwable) { Napier.e("❌", e) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = e.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = e.message)) } finally { mutableStateFlow.update(SecurityResult::hideLoading) } diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModel.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModel.kt index df461cc6..df4bc678 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModel.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/signin/SignInViewModel.kt @@ -25,14 +25,14 @@ class SignInViewModel( mutableStateFlow.value = when { pass.isEmpty() -> SignInResult.ShowEmptyPassError checkPasswordUseCase(pass) -> { - router.navigateClearingBackStack(AppNavGraph.Main.name) + router.navigateClearingBackStack(AppNavGraph.Main) SignInResult.ShowSignInForm } else -> SignInResult.ShowIncorrectPassError } } catch (error: Throwable) { Napier.e("❌", error) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = error.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = error.message)) mutableStateFlow.value = SignInResult.ShowSignInForm } } diff --git a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModel.kt b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModel.kt index 1ea78a93..38402d6f 100644 --- a/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModel.kt +++ b/shared/src/commonMain/kotlin/com/softartdev/notedelight/shared/presentation/splash/SplashViewModel.kt @@ -22,13 +22,13 @@ class SplashViewModel( try { router.navigateClearingBackStack( route = when (safeRepo.databaseState) { - PlatformSQLiteState.ENCRYPTED -> AppNavGraph.SignIn.name - else -> AppNavGraph.Main.name + PlatformSQLiteState.ENCRYPTED -> AppNavGraph.SignIn + else -> AppNavGraph.Main } ) } catch (error: Throwable) { Napier.e("❌", error) - router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = error.message)) + router.navigate(route = AppNavGraph.ErrorDialog(message = error.message)) } mutableStateFlow.value = false } diff --git a/shared/src/jvmTest/kotlin/com/softartdev/notedelight/shared/navigation/RouterStub.kt b/shared/src/jvmTest/kotlin/com/softartdev/notedelight/shared/navigation/RouterStub.kt index 5916af7d..557b7607 100644 --- a/shared/src/jvmTest/kotlin/com/softartdev/notedelight/shared/navigation/RouterStub.kt +++ b/shared/src/jvmTest/kotlin/com/softartdev/notedelight/shared/navigation/RouterStub.kt @@ -8,12 +8,12 @@ class RouterStub : Router { override fun releaseController() = Napier.d(message = "releaseController") - override fun navigate(route: String) = Napier.d(message = "navigate: $route") + override fun navigate(route: T) = Napier.d(message = "navigate: $route") - override fun navigateClearingBackStack(route: String) = + override fun navigateClearingBackStack(route: T) = Napier.d("navigateClearingBackStack: $route") - override fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean { + override fun popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean { Napier.d("popBackStack: $route, inclusive: $inclusive, saveState: $saveState") return true }