From 0a871a137cbb05af02a8dde48a085336a2cffd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Grebe-L=C3=BCth?= Date: Sun, 17 Aug 2025 18:01:00 +0200 Subject: [PATCH 1/2] migrate to navigation3 --- CHANGELOG.md | 4 + README.md | 3 +- app/build.gradle.kts | 13 +- .../composedex/navigation/NavigationTest.kt | 51 +++-- .../de/entikore/composedex/ComposeDexApp.kt | 3 +- .../entikore/composedex/ComposeDexAppState.kt | 9 +- .../ui/navigation/ComposeDexDrawer.kt | 16 +- .../composedex/ui/navigation/DrawerNavHost.kt | 196 +++++++++++------- .../ui/navigation/NavigationActions.kt | 49 ----- .../destination/ComposeDexDestination.kt | 86 ++++++++ .../ui/navigation/destination/Route.kt | 98 --------- .../ui/screen/pokemon/PokemonViewModel.kt | 14 +- .../ui/screen/type/TypeViewModel.kt | 10 +- .../ui/screen/pokemon/PokemonViewModelTest.kt | 96 +-------- .../ui/screen/type/TypeViewModelTest.kt | 89 +------- gradle/libs.versions.toml | 14 +- settings.gradle.kts | 3 + 17 files changed, 286 insertions(+), 468 deletions(-) delete mode 100644 app/src/main/kotlin/de/entikore/composedex/ui/navigation/NavigationActions.kt create mode 100644 app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/ComposeDexDestination.kt delete mode 100644 app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/Route.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0766e0d..ba5f775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- migrate to navigation3 library. + ### Fixed - asynchronous DAO queries no longer suspend indefinitely if table is empty. diff --git a/README.md b/README.md index fe14248..9dd0939 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ The data layer is designed with an offline-first approach in mind. There exists - [AndroidX Drawerlayout](https://developer.android.com/jetpack/androidx/releases/drawerlayout): Provides a UI panel that slides in from the edge of the screen, used for the navigation menu. - [AndroidX ExoPlayer](https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer): An application-level media player for Android to play audio. - [AndroidX Hilt](https://developer.android.com/jetpack/androidx/releases/hilt): A dependency injection library to integrate Dagger with Android components. -- [AndroidX Navigation](https://developer.android.com/jetpack/androidx/releases/navigation): A framework for navigating between different screens, providing a structured way to manage navigation flows and transitions. +- [AndroidX navigation3](https://developer.android.com/jetpack/androidx/releases/navigation3): + Modern, type-safe navigation patterns for navigating between different composable screens. - [AndroidX Room](https://developer.android.com/jetpack/androidx/releases/room): An abstraction layer over SQLite, providing a convenient and type-safe way to interact with the app's database. - [Coil](https://github.com/coil-kt/coil): An image loading and caching library for Android. - [ksp](https://github.com/google/ksp): Kotlin Symbol Processing API. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2875152..e2e2a9b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.compose.compiler) alias(libs.plugins.junit5) + alias(libs.plugins.jetbrains.kotlin.serialization) } android { @@ -18,8 +19,8 @@ android { applicationId = "de.entikore.composedex" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "2.0.0" testInstrumentationRunner = "de.entikore.composedex.HiltTestRunner" } @@ -57,7 +58,6 @@ android { } } - dependencies { detektPlugins(libs.detekt.formatting) detektPlugins(libs.detekt.compose) @@ -74,11 +74,15 @@ dependencies { implementation(libs.androidx.core.ktx) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.kotlinx.serialization.core) + implementation(libs.hilt.android) ksp(libs.hilt.compiler) implementation(libs.androidx.datastore.preferences) - implementation(libs.androidx.navigation) implementation(libs.androidx.hilt.navigation) implementation(libs.androidx.room) @@ -127,7 +131,6 @@ dependencies { androidTestImplementation(libs.androidx.compose.material3) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.rules) - androidTestImplementation(libs.androidx.navigation.test) androidTestImplementation(libs.mock.webserver) debugImplementation(libs.androidx.compose.ui.manifest) androidTestImplementation(libs.hilt.android.testing) diff --git a/app/src/androidTest/kotlin/de/entikore/composedex/navigation/NavigationTest.kt b/app/src/androidTest/kotlin/de/entikore/composedex/navigation/NavigationTest.kt index 2238b7a..612b90d 100644 --- a/app/src/androidTest/kotlin/de/entikore/composedex/navigation/NavigationTest.kt +++ b/app/src/androidTest/kotlin/de/entikore/composedex/navigation/NavigationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,26 +21,23 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.testing.TestNavHostController import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import de.entikore.composedex.ComposeDexAppState import de.entikore.composedex.MainActivity import de.entikore.composedex.R -import de.entikore.composedex.ui.navigation.destination.ComposeDexDestination -import de.entikore.composedex.ui.navigation.destination.Favourite -import de.entikore.composedex.ui.navigation.destination.Generation -import de.entikore.composedex.ui.navigation.destination.Pokemon -import de.entikore.composedex.ui.navigation.destination.Settings -import de.entikore.composedex.ui.navigation.destination.Type import de.entikore.composedex.onNodeWithTagStringId import de.entikore.composedex.ui.navigation.DrawerNavHost +import de.entikore.composedex.ui.navigation.destination.ComposeDexDestination +import de.entikore.composedex.ui.navigation.destination.FavouriteDestination +import de.entikore.composedex.ui.navigation.destination.GenerationDestination +import de.entikore.composedex.ui.navigation.destination.PokemonDestination +import de.entikore.composedex.ui.navigation.destination.SettingsDestination +import de.entikore.composedex.ui.navigation.destination.TypeDestination import org.junit.Before import org.junit.Rule import org.junit.Test @@ -54,23 +51,16 @@ class NavigationTest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() - private lateinit var navController: TestNavHostController - @Before fun setupNavGraph() { hiltRule.inject() composeTestRule.activity.setContent { - navController = TestNavHostController(LocalContext.current).apply { - navigatorProvider.addNavigator(ComposeNavigator()) - } val appState = ComposeDexAppState( snackbarHostState = remember { SnackbarHostState() }, - navController = remember { navController }, scope = rememberCoroutineScope(), drawerState = rememberDrawerState(DrawerValue.Closed) ) DrawerNavHost( - navController = appState.navController, drawerState = appState.drawerState, snackBarHostState = appState.snackbarHostState, changeDrawerState = appState::changeDrawerState, @@ -88,28 +78,43 @@ class NavigationTest { @Test fun navGraph_clickOnDrawerPokemon_navigateToPokemonScreen() { - navigateToDrawerScreen(Favourite, R.string.test_tag_compose_dex_destination_favourite) - navigateToDrawerScreen(Pokemon, R.string.test_tag_compose_dex_destination_pokemon) + navigateToDrawerScreen( + FavouriteDestination(), + R.string.test_tag_compose_dex_destination_favourite + ) + navigateToDrawerScreen( + PokemonDestination(), + R.string.test_tag_compose_dex_destination_pokemon + ) } @Test fun navGraph_clickOnDrawerFavourite_navigateToFavouriteScreen() { - navigateToDrawerScreen(Favourite, R.string.test_tag_compose_dex_destination_favourite) + navigateToDrawerScreen( + FavouriteDestination(), + R.string.test_tag_compose_dex_destination_favourite + ) } @Test fun navGraph_clickOnDrawerGeneration_navigateToGenerationScreen() { - navigateToDrawerScreen(Generation, R.string.test_tag_compose_dex_destination_generation) + navigateToDrawerScreen( + GenerationDestination(), + R.string.test_tag_compose_dex_destination_generation + ) } @Test fun navGraph_clickOnDrawerType_navigateToTypeScreen() { - navigateToDrawerScreen(Type, R.string.test_tag_compose_dex_destination_type) + navigateToDrawerScreen(TypeDestination(), R.string.test_tag_compose_dex_destination_type) } @Test fun navGraph_clickOnDrawerSettings_navigateToSettingsScreen() { - navigateToDrawerScreen(Settings, R.string.test_tag_compose_dex_destination_settings) + navigateToDrawerScreen( + SettingsDestination(), + R.string.test_tag_compose_dex_destination_settings + ) } private fun navigateToDrawerScreen( diff --git a/app/src/main/kotlin/de/entikore/composedex/ComposeDexApp.kt b/app/src/main/kotlin/de/entikore/composedex/ComposeDexApp.kt index c335c75..cba52df 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ComposeDexApp.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ComposeDexApp.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ fun ComposeDexApp(typeTheme: TypeThemeConfig, isInDarkTheme: Boolean) { ComposeDexTheme(appTheme = typeTheme, isDarkMode = isInDarkTheme) { DrawerNavHost( - navController = appState.navController, drawerState = appState.drawerState, snackBarHostState = appState.snackbarHostState, changeDrawerState = appState::changeDrawerState, diff --git a/app/src/main/kotlin/de/entikore/composedex/ComposeDexAppState.kt b/app/src/main/kotlin/de/entikore/composedex/ComposeDexAppState.kt index 978b54d..0e0ca0a 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ComposeDexAppState.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ComposeDexAppState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -31,7 +29,6 @@ import kotlinx.coroutines.launch * Models state for the [MainActivity]. */ data class ComposeDexAppState( - val navController: NavHostController, val drawerState: DrawerState, private val scope: CoroutineScope, val snackbarHostState: SnackbarHostState @@ -53,13 +50,11 @@ data class ComposeDexAppState( @Composable fun rememberComposeDexAppState( snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, - navController: NavHostController = rememberNavController(), coroutineScope: CoroutineScope = rememberCoroutineScope(), drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed) -) = remember(snackbarHostState, navController, coroutineScope, drawerState) { +) = remember(snackbarHostState, coroutineScope, drawerState) { ComposeDexAppState( snackbarHostState = snackbarHostState, - navController = navController, scope = coroutineScope, drawerState = drawerState ) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/ComposeDexDrawer.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/ComposeDexDrawer.kt index 33e54a7..b7bed64 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/ComposeDexDrawer.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/ComposeDexDrawer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,16 +35,16 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry import coil3.compose.AsyncImage import de.entikore.composedex.R import de.entikore.composedex.ui.component.cutCornerShapeBorder +import de.entikore.composedex.ui.navigation.destination.ComposeDexDestination import de.entikore.composedex.ui.navigation.destination.drawerScreens @Composable fun ComposeDexDrawer( - currentlySelected: NavBackStackEntry?, - onDestinationClick: (route: String) -> Unit, + currentlySelected: ComposeDexDestination?, + onDestinationClick: (route: ComposeDexDestination) -> Unit, modifier: Modifier = Modifier ) { ModalDrawerSheet( @@ -108,8 +108,8 @@ fun DrawerHead(modifier: Modifier = Modifier) { @Composable fun DrawerBody( - onDestinationClick: (route: String) -> Unit, - currentlySelected: NavBackStackEntry?, + onDestinationClick: (ComposeDexDestination) -> Unit, + currentlySelected: ComposeDexDestination?, modifier: Modifier = Modifier ) { Column( @@ -122,8 +122,8 @@ fun DrawerBody( DrawerEntry( icon = entry.icon, name = entry.uiName, - selected = entry.route == (currentlySelected?.destination?.route ?: ""), - onClick = { onDestinationClick(entry.route) }, + selected = entry.javaClass == currentlySelected?.javaClass, + onClick = { onDestinationClick(entry) }, modifier = Modifier .padding(dimensionResource(id = R.dimen.small_padding)) ) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt index 968c5c6..abdc1b2 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package de.entikore.composedex.ui.navigation +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.DrawerState @@ -24,48 +27,51 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import de.entikore.composedex.R -import de.entikore.composedex.ui.navigation.destination.Favourite -import de.entikore.composedex.ui.navigation.destination.Generation -import de.entikore.composedex.ui.navigation.destination.Pokemon -import de.entikore.composedex.ui.navigation.destination.Settings -import de.entikore.composedex.ui.navigation.destination.Type +import de.entikore.composedex.ui.navigation.destination.ComposeDexDestination +import de.entikore.composedex.ui.navigation.destination.FavouriteDestination +import de.entikore.composedex.ui.navigation.destination.GenerationDestination +import de.entikore.composedex.ui.navigation.destination.PokemonDestination +import de.entikore.composedex.ui.navigation.destination.SettingsDestination +import de.entikore.composedex.ui.navigation.destination.TypeDestination import de.entikore.composedex.ui.screen.favourite.FavouriteScreen import de.entikore.composedex.ui.screen.generation.GenerationScreen import de.entikore.composedex.ui.screen.pokemon.PokemonScreen +import de.entikore.composedex.ui.screen.pokemon.PokemonViewModel import de.entikore.composedex.ui.screen.setting.SettingsScreen import de.entikore.composedex.ui.screen.type.TypeScreen +import de.entikore.composedex.ui.screen.type.TypeViewModel @Composable fun DrawerNavHost( - navController: NavHostController, drawerState: DrawerState, snackBarHostState: SnackbarHostState, changeDrawerState: () -> Unit, showSnackbar: (String) -> Unit, modifier: Modifier = Modifier ) { - val navActions: NavigationActions = remember(navController) { - NavigationActions(navController) - } - val currentNavBackStackEntry by navController.currentBackStackEntryAsState() + val backstack = remember { mutableStateListOf(PokemonDestination()) } + val currentScreenObjectForNavDisplay: ComposeDexDestination? = backstack.lastOrNull() ModalNavigationDrawer( drawerContent = { ComposeDexDrawer( - currentlySelected = currentNavBackStackEntry, + currentlySelected = currentScreenObjectForNavDisplay, onDestinationClick = { route -> changeDrawerState() - navController.navigate(route) + backstack.add(route) } ) }, @@ -79,67 +85,107 @@ fun DrawerNavHost( containerColor = MaterialTheme.colorScheme.background, modifier = modifier.fillMaxSize() ) { padding -> - NavHost(navController = navController, startDestination = Pokemon.routeWithArgs) { - composable( - route = Pokemon.routeWithArgs, - arguments = Pokemon.arguments - ) { - PokemonScreen( - openDrawer = changeDrawerState, - navigateToTypes = { type: String -> - navActions.navigateToTypeScreen(type) - }, - modifier = Modifier.fillMaxSize() - .testTag(stringResource(R.string.test_tag_compose_dex_destination_pokemon)) - .padding(padding) - ) - } - composable(route = Favourite.route) { - FavouriteScreen( - navigateToPokemon = { pokemon: String -> - navActions.navigateToPokemonScreen(pokemon) - }, - openDrawer = changeDrawerState, - modifier = Modifier.fillMaxSize().testTag( - stringResource(R.string.test_tag_compose_dex_destination_favourite) + + NavDisplay( + entryDecorators = listOf( + // Add the default decorators for managing scenes and saving state + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + // Then add the view model store decorator + rememberViewModelStoreNavEntryDecorator() + ), + backStack = backstack, + onBack = { backstack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { pokemonDestination -> + PokemonScreen( + openDrawer = changeDrawerState, + navigateToTypes = { type: String -> + backstack.add(TypeDestination(typeName = type)) + }, + viewModel = hiltViewModel().also { + pokemonDestination.pokemonName?.let { name -> + it.lookUpPokemon(name) + } + }, + modifier = Modifier + .fillMaxSize() + .testTag(stringResource(R.string.test_tag_compose_dex_destination_pokemon)) + .padding(padding) ) - ) - } - composable(route = Generation.route) { - GenerationScreen( - navigateToPokemon = { pokemon: String -> - navActions.navigateToPokemonScreen(pokemon) - }, - openDrawer = changeDrawerState, - modifier = Modifier.fillMaxSize().testTag( - stringResource(R.string.test_tag_compose_dex_destination_generation) + } + entry { + FavouriteScreen( + navigateToPokemon = { pokemon: String -> + backstack.add(PokemonDestination(pokemonName = pokemon)) + }, + openDrawer = changeDrawerState, + modifier = Modifier + .fillMaxSize() + .testTag( + stringResource(R.string.test_tag_compose_dex_destination_favourite) + ) ) - ) - } - composable( - route = Type.routeWithArgs, - arguments = Type.arguments - ) { - TypeScreen( - navigateToPokemon = { pokemon: String -> - navActions.navigateToPokemonScreen(pokemon) - }, - openDrawer = changeDrawerState, - modifier = Modifier.fillMaxSize().testTag( - stringResource(R.string.test_tag_compose_dex_destination_type) + } + entry { + GenerationScreen( + navigateToPokemon = { pokemon: String -> + backstack.add(PokemonDestination(pokemonName = pokemon)) + }, + openDrawer = changeDrawerState, + modifier = Modifier + .fillMaxSize() + .testTag( + stringResource(R.string.test_tag_compose_dex_destination_generation) + ) ) - ) - } - composable(route = Settings.route) { - SettingsScreen( - openDrawer = changeDrawerState, - showSnackbar = showSnackbar, - modifier = Modifier.fillMaxSize().testTag( - stringResource(R.string.test_tag_compose_dex_destination_settings) + } + entry { typeDestination -> + TypeScreen( + navigateToPokemon = { pokemon: String -> + backstack.add(PokemonDestination(pokemonName = pokemon)) + }, + openDrawer = changeDrawerState, + viewModel = hiltViewModel().also { + typeDestination.typeName?.let { name -> + it.fetchType(name) + } + }, + modifier = Modifier + .fillMaxSize() + .testTag( + stringResource(R.string.test_tag_compose_dex_destination_type) + ) ) - ) - } - } + } + entry { + SettingsScreen( + openDrawer = changeDrawerState, + showSnackbar = showSnackbar, + modifier = Modifier + .fillMaxSize() + .testTag( + stringResource(R.string.test_tag_compose_dex_destination_settings) + ) + ) + } + }, + transitionSpec = { + // Slide in from right when navigating forward + slideInHorizontally(initialOffsetX = { it }) togetherWith + slideOutHorizontally(targetOffsetX = { -it }) + }, + popTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally(initialOffsetX = { -it }) togetherWith + slideOutHorizontally(targetOffsetX = { it }) + }, + predictivePopTransitionSpec = { + // Slide in from left when navigating back + slideInHorizontally(initialOffsetX = { -it }) togetherWith + slideOutHorizontally(targetOffsetX = { it }) + }, + ) } } } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/NavigationActions.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/NavigationActions.kt deleted file mode 100644 index 37cecde..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/NavigationActions.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.ui.navigation - -import androidx.navigation.NavHostController -import de.entikore.composedex.ui.navigation.destination.Pokemon -import de.entikore.composedex.ui.navigation.destination.Type - -/** - * Provides navigation actions for navigating between screens. - * - * This class encapsulates navigation logic using a [NavHostController] to - * navigate to different screens with arguments, such as the Type screen and the Pokémon screen. - * - * @param navController The [NavHostController] used for performing navigation. - */ -class NavigationActions(private val navController: NavHostController) { - fun navigateToTypeScreen( - typeName: String? - ) { - val route = typeName?.let { "${Type.route}?${Type.typeArg}=$it" } ?: Type.route - navController.navigate( - route - ) - } - - fun navigateToPokemonScreen( - pokemonNameOrId: String? - ) { - val route = - pokemonNameOrId?.let { "${Pokemon.route}?${Pokemon.pokemonArg}=$it" } ?: Pokemon.route - navController.navigate( - route - ) - } -} diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/ComposeDexDestination.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/ComposeDexDestination.kt new file mode 100644 index 0000000..4c63bfd --- /dev/null +++ b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/ComposeDexDestination.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Entikore + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.entikore.composedex.ui.navigation.destination + +import androidx.navigation3.runtime.NavKey +import de.entikore.composedex.R +import kotlinx.serialization.Serializable + +/** + * Represents a destination within the ComposeDex application. + * + * Each destination is characterized by an icon, a route for navigation, and a user-friendly name. + */ +sealed interface ComposeDexDestination : NavKey { + val icon: Int + val uiName: String +} + +/** + * Destination for [de.entikore.composedex.ui.screen.pokemon.PokemonScreen]. + */ +@Serializable +data class PokemonDestination( + override val icon: Int = R.drawable.ic_jiggly_pixel, + override val uiName: String = "Pokemon", + val pokemonName: String? = null +) : ComposeDexDestination, NavKey + +/** + * Destination for [de.entikore.composedex.ui.screen.favourite.FavouriteDestination]. + */ +@Serializable +data class FavouriteDestination( + override val icon: Int = R.drawable.ic_star_pixel, + override val uiName: String = "Favourites" +) : ComposeDexDestination + +/** + * Destination for [de.entikore.composedex.ui.screen.generation.GenerationScreen]. + */ +@Serializable +data class GenerationDestination( + override val icon: Int = R.drawable.ic_balls_pixel, + override val uiName: String = "Generations" +) : ComposeDexDestination + +/** + * Destination for [de.entikore.composedex.ui.screen.type.TypeScreen]. + */ +@Serializable +data class TypeDestination( + override val icon: Int = R.drawable.ic_eevee_pixel, + override val uiName: String = "Types", + val typeName: String? = null +) : ComposeDexDestination + +/** + * Destination for [de.entikore.composedex.ui.screen.setting.SettingsScreen]. + */ +@Serializable +data class SettingsDestination( + override val icon: Int = R.drawable.ic_settings_pixel, + override val uiName: String = "Settings" +) : ComposeDexDestination + +val drawerScreens: List = + listOf( + PokemonDestination(), + FavouriteDestination(), + GenerationDestination(), + TypeDestination(), + SettingsDestination() + ) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/Route.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/Route.kt deleted file mode 100644 index 77733c6..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/destination/Route.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.ui.navigation.destination - -import androidx.navigation.NavType -import androidx.navigation.navArgument -import de.entikore.composedex.R - -/** - * Represents a destination within the ComposeDex application. - * - * Each destination is characterized by an icon, a route for navigation, and a user-friendly name. - */ -sealed interface ComposeDexDestination { - val icon: Int - val route: String - val uiName: String -} - -data object Pokemon : ComposeDexDestination { - override val icon: Int - get() = R.drawable.ic_jiggly_pixel - override val route: String - get() = "pokemon" - override val uiName: String - get() = "Pokemon" - - const val pokemonArg = "pokemon_name_or_id" - val routeWithArgs = "$route?$pokemonArg={$pokemonArg}" - val arguments = listOf( - navArgument(pokemonArg) { - type = NavType.StringType - nullable = true - defaultValue = null - } - ) -} - -data object Favourite : ComposeDexDestination { - override val icon: Int - get() = R.drawable.ic_star_pixel - override val route: String - get() = "favourite" - override val uiName: String - get() = "Favourites" -} - -data object Generation : ComposeDexDestination { - override val icon: Int - get() = R.drawable.ic_balls_pixel - override val route: String - get() = "generation" - override val uiName: String - get() = "Generations" -} - -data object Type : ComposeDexDestination { - override val icon: Int - get() = R.drawable.ic_eevee_pixel - override val route: String - get() = "type" - override val uiName: String - get() = "Types" - - const val typeArg = "type_name" - val routeWithArgs = "$route?$typeArg={$typeArg}" - val arguments = listOf( - navArgument(typeArg) { - type = NavType.StringType - nullable = true - defaultValue = null - } - ) -} - -data object Settings : ComposeDexDestination { - override val icon: Int - get() = R.drawable.ic_settings_pixel - override val route: String - get() = "setting" - override val uiName: String - get() = "Settings" -} - -val drawerScreens = listOf(Pokemon, Favourite, Generation, Type, Settings) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt index 9cf5a0a..91ba341 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package de.entikore.composedex.ui.screen.pokemon -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem @@ -49,7 +48,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.text.get /** * Manages application state for the [PokemonScreen]. @@ -62,8 +60,7 @@ class PokemonViewModel @Inject constructor( private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val changeThemeUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val exoPlayer: ExoPlayer, - private val tts: ComposeDexTTS, - savedStateHandle: SavedStateHandle + private val tts: ComposeDexTTS ) : ViewModel() { private val _selectedPokemonFlow = MutableStateFlow(null) @@ -249,15 +246,8 @@ class PokemonViewModel @Inject constructor( return varieties.filterNotNull().toList() } - private val pokemonNameOrId: String? = savedStateHandle[ - de.entikore.composedex.ui.navigation.destination.Pokemon.pokemonArg - ] - init { exoPlayer.repeatMode = Player.REPEAT_MODE_OFF - if (!pokemonNameOrId.isNullOrEmpty()) { - lookUpPokemon(pokemonNameOrId) - } } override fun onCleared() { diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt index a1eda10..8b16d7f 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package de.entikore.composedex.ui.screen.type -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.entikore.composedex.domain.WorkResult @@ -56,8 +55,7 @@ class TypeViewModel @Inject constructor( getTypeUseCase: GetTypeUseCase, getPokemonOfTypeUseCase: GetPokemonOfTypeUseCase, private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - savedStateHandle: SavedStateHandle + private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase ) : PokemonFilterViewModel() { private val _selectedTypeFlow = MutableStateFlow(null) @@ -93,10 +91,6 @@ class TypeViewModel @Inject constructor( Success() ) - init { - _selectedTypeFlow.value = savedStateHandle[de.entikore.composedex.ui.navigation.destination.Type.typeArg] - } - fun fetchType(typeName: String) { Timber.d("Search for type $typeName") _selectedTypeFlow.value = typeName diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt index 0a6f2bd..4fa65af 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,10 +47,8 @@ import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource -import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.mock -import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) @@ -101,97 +99,13 @@ class PokemonViewModelTest { setAsFavouriteUseCase, changeThemeUseCase, mockPlayer, - mockTTS, - mockSavedStateHandle + mockTTS ) val expectedState = PokemonScreenState.NoPokemonSelected assertThat(viewModel.screenState.value).isEqualTo(expectedState) } - @Test - fun `creating PokemonDetailViewModel with SavedStateHandle exposes Success PokemonDetailScreenUiState with expected Pokemon`() = - runTest { - val expectedPokemon = - getPokemonInfoRemote(getTestModel(POKEMON_GLOOM_NAME)).toEntity().asExternalModel() - val oddish = - getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() - val vileplume = getPokemonInfoRemote(getTestModel(POKEMON_VILEPLUME_NAME)).toEntity() - .asExternalModel() - val bellossom = getPokemonInfoRemote(getTestModel(POKEMON_BELLOSSOM_NAME)).toEntity() - .asExternalModel() - fakePokemonRepository.addPokemon(expectedPokemon, oddish, vileplume, bellossom) - whenever(mockSavedStateHandle.get(anyString())).thenReturn(POKEMON_GLOOM_NAME) - - viewModel = PokemonViewModel( - GetPokemonUseCase(fakePokemonRepository), - saveRemoteImageUseCase, - saveRemoteCryUseCase, - setAsFavouriteUseCase, - changeThemeUseCase, - mockPlayer, - mockTTS, - mockSavedStateHandle - ) - - val expectedState = PokemonScreenState.Success( - selectedPokemon = expectedPokemon.processFlavourTextEntries(), - evolvesFrom = oddish.toPokemonPreview(evolvesTo = false), - evolvesTo = listOf( - vileplume.toPokemonPreview(true), - bellossom.toPokemonPreview(true) - ), - varieties = listOf(expectedPokemon.processFlavourTextEntries()) - ) - - viewModel.screenState.test { - var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.NoPokemonSelected::class.java) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Loading::class.java) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Success::class.java) - assertThat(stateResult).isEqualTo( - expectedState - ) - } - } - - @Test - fun `creating PokemonDetailViewModel with unknown SavedStateHandle exposes Error PokemonDetailScreenUiState`() = - runTest { - whenever(mockSavedStateHandle.get(anyString())).thenReturn(POKEMON_GLOOM_NAME) - - viewModel = PokemonViewModel( - GetPokemonUseCase(fakePokemonRepository), - saveRemoteImageUseCase, - saveRemoteCryUseCase, - setAsFavouriteUseCase, - changeThemeUseCase, - mockPlayer, - mockTTS, - mockSavedStateHandle - ) - val expectedState = - PokemonScreenState.Error("${PokemonViewModel.Companion.ERROR_LOADING_POKEMON} $POKEMON_GLOOM_NAME") - - viewModel.screenState.test { - var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.NoPokemonSelected::class.java) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Loading::class.java) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Error::class.java) - assertThat(stateResult).isEqualTo( - expectedState - ) - } - } - @ParameterizedTest @MethodSource("searchTerms") fun `search for pokemon exposes Success PokemonDetailScreenUiState with expected Pokemon`( @@ -208,8 +122,7 @@ class PokemonViewModelTest { setAsFavouriteUseCase, changeThemeUseCase, mockPlayer, - mockTTS, - mockSavedStateHandle + mockTTS ) viewModel.screenState.test { @@ -239,8 +152,7 @@ class PokemonViewModelTest { setAsFavouriteUseCase, changeThemeUseCase, mockPlayer, - mockTTS, - mockSavedStateHandle + mockTTS ) val expectedState = PokemonScreenState.Error( diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt index d8972c3..356bd6d 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,9 +41,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock -import org.mockito.Mockito.anyString import org.mockito.Mockito.mock -import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) @@ -78,8 +76,7 @@ class TypeViewModelTest { typeUseCase, getPokemonOfTypeUseCase, saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle + setAsFavouriteUseCase ) val expectedState = TypeScreenUiState.Success() @@ -101,8 +98,7 @@ class TypeViewModelTest { typeUseCase, getPokemonOfTypeUseCase, saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle + setAsFavouriteUseCase ) val expectedState = TypeScreenUiState.Success( @@ -121,79 +117,6 @@ class TypeViewModelTest { } } - @Test - fun `creating TypeViewModel with SavedStateHandle exposes Success SelectedTypeUiState with expected Type`() = runTest { - val expectedType = getTypeRemote(TYPE_ICE_FILE).toEntity().asExternalModel() - fakeTypeRepository.addTypes(expectedType) - whenever(mockSavedStateHandle.get(anyString())).thenReturn(TYPE_ICE_NAME) - - viewModel = TypeViewModel( - typesUseCase, - typeUseCase, - getPokemonOfTypeUseCase, - saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle - ) - - val expectedState = TypeScreenUiState.Success( - types = listOf(getTypeRemote(TYPE_ICE_FILE).toEntity().asExternalModel()), - selectedType = SelectedTypeUiState.Success( - selectedType = getTypeRemote(TYPE_ICE_FILE).toEntity().asExternalModel(), - pokemonState = PokemonUiState.Success(emptyList()), - showLoadingItem = true - ) - ) - - viewModel.screenState.test { - var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(expectedState.copy(selectedType = SelectedTypeUiState.Loading)) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(expectedState) - } - } - - @Test - fun `creating TypeViewModel with unknown SavedStateHandle exposes Error SelectedTypeUiState`() = runTest { - whenever(mockSavedStateHandle.get(anyString())).thenReturn("unknown") - - viewModel = TypeViewModel( - typesUseCase, - typeUseCase, - getPokemonOfTypeUseCase, - saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle - ) - - val expectedState = TypeScreenUiState.Success( - selectedType = SelectedTypeUiState.Error - ) - - viewModel.screenState.test { - var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat( - stateResult - ).isEqualTo(TypeScreenUiState.Success().copy(selectedType = SelectedTypeUiState.Loading)) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(expectedState) - } - } - @Test fun `search for a type exposes an Success TypeScreenUiState with expected type`() = runTest { val iceType = getTypeRemote(TYPE_ICE_FILE).toEntity().asExternalModel() @@ -207,8 +130,7 @@ class TypeViewModelTest { typeUseCase, getPokemonOfTypeUseCase, saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle + setAsFavouriteUseCase ) val expectedState = TypeScreenUiState.Success( @@ -259,8 +181,7 @@ class TypeViewModelTest { typeUseCase, getPokemonOfTypeUseCase, saveRemoteImageUseCase, - setAsFavouriteUseCase, - mockSavedStateHandle + setAsFavouriteUseCase ) val expectedState = TypeScreenUiState.Success( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bddb10f..0cce9b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,13 +27,16 @@ junit5 = "5.13.2" junitVersion = "1.2.1" konsist = "0.17.3" kotlinAndroid = "2.2.0" +kotlinSerialization = "2.1.21" +kotlinxSerializationCore = "1.8.1" ksp = "2.2.0-2.0.2" +lifecycleViewmodelNav3 = "1.0.0-alpha01" mockitoCore = "5.18.0" mockitoAndroid = "5.18.0" mockitoKotlin = "5.4.0" mockwebserver = "4.12.0" moshi = "1.15.2" -navigation = "2.9.0" +nav3Core = "1.0.0-alpha01" retrofit = "3.0.0" room = "2.7.2" timber = "5.0.1" @@ -53,8 +56,9 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = androidx-core-splash = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplash" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStore" } androidx-hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" } -androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } -androidx-navigation-test = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigation" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } androidx-room = { group = "androidx.room", name = "room-runtime", version.ref = "room" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } @@ -96,6 +100,7 @@ jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", v jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit5" } konsist = { group = "com.lemonappdev", name = "konsist", version.ref = "konsist" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockitoCore" } mockito-android = { group = "org.mockito", name = "mockito-android", version.ref = "mockitoAndroid" } @@ -111,6 +116,7 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinAndroid" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } +jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" } junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "junit5Plugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinAndroid" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index d89b40d..664fc9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { + url = uri("https://androidx.dev/snapshots/builds/13508953/artifacts/repository") + } } } From ade6e5b49d665cda46f716b6cf9331d330e0b002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Grebe-L=C3=BCth?= Date: Sun, 17 Aug 2025 18:22:25 +0200 Subject: [PATCH 2/2] fix detect findings --- .../de/entikore/composedex/ui/navigation/DrawerNavHost.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt index abdc1b2..57d0298 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/navigation/DrawerNavHost.kt @@ -173,17 +173,17 @@ fun DrawerNavHost( transitionSpec = { // Slide in from right when navigating forward slideInHorizontally(initialOffsetX = { it }) togetherWith - slideOutHorizontally(targetOffsetX = { -it }) + slideOutHorizontally(targetOffsetX = { -it }) }, popTransitionSpec = { // Slide in from left when navigating back slideInHorizontally(initialOffsetX = { -it }) togetherWith - slideOutHorizontally(targetOffsetX = { it }) + slideOutHorizontally(targetOffsetX = { it }) }, predictivePopTransitionSpec = { // Slide in from left when navigating back slideInHorizontally(initialOffsetX = { -it }) togetherWith - slideOutHorizontally(targetOffsetX = { it }) + slideOutHorizontally(targetOffsetX = { it }) }, ) }