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 fda640b commit 3629660
Show file tree
Hide file tree
Showing 26 changed files with 226 additions and 217 deletions.
1 change: 1 addition & 0 deletions android-compose-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ dependencies {
androidTestImplementation(compose.desktop.uiTestJUnit4)
androidTestImplementation(libs.turbine)
androidTestImplementation(libs.leakCanary.android.instrumentation)
lintChecks(libs.android.security.lint)
}
tasks.withType<UploadMappingFileTask>{
dependsOn("processDebugGoogleServices")
Expand Down
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
11 changes: 8 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ kotlin = "2.0.21"
agp = "8.7.1"
gms = "4.4.2"
crashlytics = "3.0.2"
compose = "1.6.11"
compose = "1.7.0"
coroutines = "1.8.1"
sqlDelight = "2.0.2"
androidxSqlite = "2.4.0"
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 All @@ -29,13 +30,14 @@ androidxArch = "2.2.0"
androidxTestExt = "1.2.1"
androidxTest = "1.6.2"
androidxTestOrchestrator = "1.5.1"
firebase = "33.4.0"
firebase = "33.5.0"
leakCanary = "2.14"
junit = "4.13.2"
mockito = "5.14.1"
turbine = "1.1.0"
espresso = "3.6.1"
desugar = "2.1.2"
androidSecurityLint = "1.0.3"
appdirs = "1.2.2"

[libraries]
Expand Down Expand Up @@ -65,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 Expand Up @@ -118,6 +121,8 @@ espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-re

desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" }

android-security-lint = { module = "com.android.security.lint:lint", version.ref = "androidSecurityLint" }

[plugins]
gradle-convention = { id = "com.softartdev.notedelight.buildlogic.convention", version = "unspecified" }

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.

194 changes: 99 additions & 95 deletions iosApp/Pods/Pods.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

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
Loading

0 comments on commit 3629660

Please sign in to comment.