Skip to content

Commit

Permalink
type-safe navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
babichev.a committed Oct 23, 2024
1 parent 95df2c9 commit 1412bc2
Show file tree
Hide file tree
Showing 25 changed files with 123 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion iosApp/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 0dc93a6f6109335ea8cd3f91d2c87cc8c99f04a3

COCOAPODS: 1.12.1
COCOAPODS: 1.15.2
2 changes: 1 addition & 1 deletion iosApp/Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions iosApp/Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions iosApp/iosApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T : Any> navigate(route: T) = runOnUiThread { router.navigate(route) }

override fun navigateClearingBackStack(route: String) = runOnUiThread {
override fun <T : Any> navigateClearingBackStack(route: T) = runOnUiThread {
router.navigateClearingBackStack(route)
}

override fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean =
override fun <T : Any> popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean =
runOnUiThread { router.popBackStack(route, inclusive, saveState) }

override fun popBackStack(): Boolean = runOnUiThread { router.popBackStack() }
Expand Down
1 change: 0 additions & 1 deletion shared-compose-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,70 +39,58 @@ fun App(
}
NavHost(
navController = navController,
startDestination = AppNavGraph.Splash.route,
startDestination = AppNavGraph.Splash,
) {
composable(route = AppNavGraph.Splash.route) {
composable<AppNavGraph.Splash> {
SplashScreen(splashViewModel = getViewModel())
}
composable(route = AppNavGraph.SignIn.route) {
composable<AppNavGraph.SignIn> {
SignInScreen(signInViewModel = getViewModel())
}
composable(route = AppNavGraph.Main.route) {
composable<AppNavGraph.Main> {
MainScreen(mainViewModel = getViewModel())
}
composable(
route = AppNavGraph.Details.route,
arguments = listOf(navArgument(name = AppNavGraph.ARG_NOTE_ID) { type = NavType.LongType })
) { backStackEntry: NavBackStackEntry ->
composable<AppNavGraph.Details> { backStackEntry: NavBackStackEntry ->
NoteDetail(
noteViewModel = getViewModel(),
noteId = AppNavGraph.ARG_NOTE_ID.let(backStackEntry.arguments!!::getLong),
noteId = backStackEntry.toRoute<AppNavGraph.Details>().noteId,
)
}
composable(route = AppNavGraph.Settings.route) {
composable<AppNavGraph.Settings> {
SettingsScreen(settingsViewModel = getViewModel())
}
dialog(route = AppNavGraph.ThemeDialog.route) {
dialog<AppNavGraph.ThemeDialog> {
val preferenceHelper: PreferenceHelper = themePrefs.preferenceHelper
ThemeDialog(
darkThemeState = themePrefs.darkThemeState,
writePref = preferenceHelper::themeEnum::set,
dismissDialog = navController::navigateUp,
)
}
dialog(route = AppNavGraph.SaveChangesDialog.route) {
dialog<AppNavGraph.SaveChangesDialog> {
SaveDialog(saveViewModel = getViewModel())
}
dialog(
route = AppNavGraph.EditTitleDialog.route,
arguments = listOf(navArgument(name = AppNavGraph.ARG_NOTE_ID) { type = NavType.LongType })
) { backStackEntry: NavBackStackEntry ->
dialog<AppNavGraph.EditTitleDialog> { backStackEntry: NavBackStackEntry ->
EditTitleDialog(
noteId = AppNavGraph.ARG_NOTE_ID.let(backStackEntry.arguments!!::getLong),
noteId = backStackEntry.toRoute<AppNavGraph.EditTitleDialog>().noteId,
editTitleViewModel = getViewModel()
)
}
dialog(route = AppNavGraph.DeleteNoteDialog.route) {
dialog<AppNavGraph.DeleteNoteDialog> {
DeleteDialog(deleteViewModel = getViewModel())
}
dialog(route = AppNavGraph.EnterPasswordDialog.route) {
dialog<AppNavGraph.EnterPasswordDialog> {
EnterPasswordDialog(enterViewModel = getViewModel())
}
dialog(route = AppNavGraph.ConfirmPasswordDialog.route) {
dialog<AppNavGraph.ConfirmPasswordDialog> {
ConfirmPasswordDialog(confirmViewModel = getViewModel())
}
dialog(route = AppNavGraph.ChangePasswordDialog.route) {
dialog<AppNavGraph.ChangePasswordDialog> {
ChangePasswordDialog(changeViewModel = getViewModel())
}
dialog(
route = AppNavGraph.ErrorDialog.route,
arguments = listOf(navArgument(name = AppNavGraph.ARG_MESSAGE) {
type = NavType.StringType
nullable = true
})
) { backStackEntry: NavBackStackEntry ->
dialog<AppNavGraph.ErrorDialog> { backStackEntry: NavBackStackEntry ->
ErrorDialog(
message = AppNavGraph.ARG_MESSAGE.let(backStackEntry.arguments!!::getString),
message = backStackEntry.toRoute<AppNavGraph.ErrorDialog>().message,
dismissDialog = navController::navigateUp
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ class RouterImpl : Router {
navController = null
}

override fun navigate(route: String) = navController!!.navigate(route)
override fun <T : Any> navigate(route: T) = navController!!.navigate(route)

override fun navigateClearingBackStack(route: String) {
override fun <T : Any> navigateClearingBackStack(route: T) {
var popped = true
while (popped) {
popped = navController!!.popBackStack()
}
navController!!.navigate(route)
}

override fun popBackStack(route: String, inclusive: Boolean, saveState: Boolean): Boolean =
override fun <T : Any> popBackStack(route: T, inclusive: Boolean, saveState: Boolean): Boolean =
navController!!.popBackStack(route, inclusive, saveState)

override fun popBackStack() = navController!!.popBackStack()
Expand Down
2 changes: 2 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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())

Expand Down
Loading

0 comments on commit 1412bc2

Please sign in to comment.