diff --git a/Readme.md b/Readme.md index 629594d..2bb24fa 100644 --- a/Readme.md +++ b/Readme.md @@ -40,6 +40,7 @@ An Offline first Android app to consume the SpaceX Backend API [`https://github. ### Navigation - [Jetpack Compose Navigation](https://developer.android.com/jetpack/compose/navigation) + - [Type Safe Navigation](https://github.com/nisrulz/android-spacex-app/pull/37) ### Networking diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 0a7bdbd..522f96a 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -69,5 +69,11 @@ gradlePlugin { implementationClass = "com.nisrulz.example.spacexapi.AndroidComposeConventionPlugin" } + + register("androidComposeNav") { + id = "spacexapi.android.compose.navigation" + implementationClass = + "com.nisrulz.example.spacexapi.AndroidComposeNavigationConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/AndroidComposeNavigationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/AndroidComposeNavigationConventionPlugin.kt new file mode 100644 index 0000000..bc1dcf5 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/AndroidComposeNavigationConventionPlugin.kt @@ -0,0 +1,18 @@ +package com.nisrulz.example.spacexapi + +import com.nisrulz.example.spacexapi.ktx.configureAndroidComposeNavigation +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidComposeNavigationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.plugin.serialization") + } + + configureAndroidComposeNavigation() + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/ktx/AndroidComposeProjectExtensions.kt b/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/ktx/AndroidComposeProjectExtensions.kt index 560b8fa..f87c9f5 100644 --- a/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/ktx/AndroidComposeProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/ktx/AndroidComposeProjectExtensions.kt @@ -27,3 +27,14 @@ internal fun Project.configureAndroidCompose() = configure { enableStrongSkippingMode = true } } + +/** + * Configure Compose Navigation options + */ +internal fun Project.configureAndroidComposeNavigation() = configure { + + dependencies { + add("implementation", catalogLibrary("navigation-compose")) + add("implementation", catalogLibrary("kotlinx-serialization-json")) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c045c8..7b86ad5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ compose-bom = "2024.06.00" #endregion # Navigation -navigationCompose = "2.7.7" +navigationCompose = "2.8.0-beta03" #region ---- Hilt hilt = "2.51.1" @@ -161,6 +161,7 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } spacexapi-android-application = { id = "spacexapi.android.application", version = "unspecified" } spacexapi-android-library = { id = "spacexapi.android.library", version = "unspecified" } spacexapi-android-compose = { id = "spacexapi.android.compose", version = "unspecified" } +spacexapi-android-compose-navigation = { id = "spacexapi.android.compose.navigation", version = "unspecified" } spacexapi-android-testing = { id = "spacexapi.android.testing", version = "unspecified" } spacexapi-android-app-hilt = { id = "spacexapi.android.app.hilt", version = "unspecified" } spacexapi-android-lib-hilt = { id = "spacexapi.android.lib.hilt", version = "unspecified" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 85146a6..9a0ca04 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.spacexapi.android.testing) alias(libs.plugins.spacexapi.android.compose) + alias(libs.plugins.spacexapi.android.compose.navigation) } android { namespace = "${ApplicationInfo.BASE_NAMESPACE}.presentation" diff --git a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/bookmarkedlaunches/BookmarkedLaunchesScreen.kt b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/bookmarkedlaunches/BookmarkedLaunchesScreen.kt index 2c2b39a..821c7f0 100644 --- a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/bookmarkedlaunches/BookmarkedLaunchesScreen.kt +++ b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/bookmarkedlaunches/BookmarkedLaunchesScreen.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.receiveAsFlow @Composable fun BookmarkedLaunchesScreen( viewModel: BookmarkedLaunchesViewModel = hiltViewModel(), - navigateBack: EmptyCallback = {}, + onBackAction: EmptyCallback = {}, navigateToDetails: SingleValueCallback = {} ) { val snackbarHostState = remember { SnackbarHostState() } @@ -35,7 +35,7 @@ fun BookmarkedLaunchesScreen( // uncompleted processing. eventFlow.receiveAsFlow().collectLatest { event -> when (event) { - BookmarkedLaunchesViewModel.UiEvent.NavigateBack -> navigateBack() + BookmarkedLaunchesViewModel.UiEvent.NavigateBack -> onBackAction() is BookmarkedLaunchesViewModel.UiEvent.NavigateToDetails -> { navigateToDetails(event.launchId) } @@ -63,8 +63,7 @@ fun BookmarkedLaunchesScreen( viewModel.navigateBack() } } else { - BookmarkedLaunchesListComponent( - state = state, + BookmarkedLaunchesListComponent(state = state, snackbarHostState = snackbarHostState, navigateToDetails = { viewModel.navigateToDetails(it) @@ -74,8 +73,7 @@ fun BookmarkedLaunchesScreen( }, navigateBack = { viewModel.navigateBack() - } - ) + }) } } } diff --git a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModel.kt b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModel.kt index 711b63f..4319a38 100644 --- a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModel.kt +++ b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModel.kt @@ -7,14 +7,12 @@ import com.nisrulz.example.spacexapi.analytics.InUseAnalytics import com.nisrulz.example.spacexapi.analytics.trackNavigateToDetail import com.nisrulz.example.spacexapi.analytics.trackScreenListOfLaunches import com.nisrulz.example.spacexapi.domain.model.LaunchInfo -import com.nisrulz.example.spacexapi.domain.usecase.GetAllBookmarkedLaunches import com.nisrulz.example.spacexapi.domain.usecase.GetAllLaunches import com.nisrulz.example.spacexapi.domain.usecase.ToggleBookmarkLaunchInfo import com.nisrulz.example.spacexapi.logger.InUseLoggers import com.nisrulz.example.spacexapi.presentation.features.listoflaunches.ListOfLaunchesViewModel.UiEvent.NavigateToDetails import com.nisrulz.example.spacexapi.presentation.features.listoflaunches.ListOfLaunchesViewModel.UiEvent.ShowSnackBar import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,15 +21,14 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class ListOfLaunchesViewModel -@Inject -constructor( +@Inject constructor( private val coroutineDispatcher: CoroutineDispatcher, private val getAllLaunches: GetAllLaunches, private val bookmarkLaunchInfo: ToggleBookmarkLaunchInfo, - private val getAllBookmarkedLaunches: GetAllBookmarkedLaunches, private val logger: InUseLoggers, private val analytics: InUseAnalytics ) : ViewModel() { @@ -50,8 +47,7 @@ constructor( @VisibleForTesting fun getListOfLaunches() = viewModelScope.launch(coroutineDispatcher) { - getAllLaunches() - .onEach { + getAllLaunches().onEach { handleListOfLaunches(it) }.catch { setError(it.message ?: "Error") diff --git a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/AppNavigation.kt b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/AppNavigation.kt index ec46bd2..32aa05c 100644 --- a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/AppNavigation.kt +++ b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/AppNavigation.kt @@ -2,43 +2,45 @@ package com.nisrulz.example.spacexapi.presentation.navigation import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.bookmarkScreen -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.detailsScreen -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.homeScreen -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.navigateBack -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.navigateToBookmarks -import com.nisrulz.example.spacexapi.presentation.navigation.NavigationRoute.navigateToLaunchDetail +import androidx.navigation.toRoute +import com.nisrulz.example.spacexapi.presentation.features.bookmarkedlaunches.BookmarkedLaunchesScreen +import com.nisrulz.example.spacexapi.presentation.features.launchdetail.LaunchDetailScreen +import com.nisrulz.example.spacexapi.presentation.features.listoflaunches.ListOfLaunchesScreen @Composable fun AppNavigation() { val navController = rememberNavController() + NavHost( - navController = navController, - startDestination = NavigationRoute.HOME_ROUTE + navController = navController, startDestination = RouteHome ) { - homeScreen( - onNavigateToDetails = { launchId -> - navController.navigateToLaunchDetail(launchId) - }, - onNavigateToBookmarks = { - navController.navigateToBookmarks() - } - ) - - bookmarkScreen( - onNavigateToDetails = { launchId -> - navController.navigateToLaunchDetail(launchId) - }, - onBackAction = { - navController.navigateBack() - } - ) - - detailsScreen( - onBackAction = { - navController.navigateBack() - } - ) + composable { + ListOfLaunchesScreen(navigateToDetails = { launchId -> + navController.navigate(RouteDetails(launchId)) + }, navigateToBookmarks = { + navController.navigate(RouteBookmark) + }) + + } + + composable { + BookmarkedLaunchesScreen(navigateToDetails = { launchId -> + navController.navigate(RouteDetails(launchId)) + }, onBackAction = { + navController.navigate(RouteHome) + }) + + } + + composable { backStackEntry -> + val id = backStackEntry.toRoute().launchId + + LaunchDetailScreen(launchId = id, onBackAction = { + navController.navigate(RouteHome) + }) + + } } -} +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/NavigationRoute.kt b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/NavigationRoute.kt index 3b55a89..418f589 100644 --- a/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/NavigationRoute.kt +++ b/presentation/src/main/java/com/nisrulz/example/spacexapi/presentation/navigation/NavigationRoute.kt @@ -1,104 +1,13 @@ package com.nisrulz.example.spacexapi.presentation.navigation -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.nisrulz.example.spacexapi.presentation.features.bookmarkedlaunches.BookmarkedLaunchesScreen -import com.nisrulz.example.spacexapi.presentation.features.launchdetail.LaunchDetailScreen -import com.nisrulz.example.spacexapi.presentation.features.listoflaunches.ListOfLaunchesScreen +import kotlinx.serialization.Serializable -/* -Read about Type safety in Navigation Compose: -https://developer.android.com/guide/navigation/design/type-safety - */ -internal object NavigationRoute { - // Home - const val HOME_ROUTE = "list_of_launches" - // Bookmark - private const val BOOKMARK_ROUTE = "bookmarked_launches" +@Serializable +object RouteHome - // Details - private const val NAV_ARG_LAUNCH_ID = "launchId" - private const val DETAILS_ROUTE = "launch_detail/{$NAV_ARG_LAUNCH_ID}" +@Serializable +object RouteBookmark - private fun buildDetailsRouteWithLaunchId(launchId: String) = DETAILS_ROUTE - .replace("{$NAV_ARG_LAUNCH_ID}", launchId) - - // Functions - private fun NavBackStackEntry.getArgLaunchId(): String = arguments - ?.getString(NAV_ARG_LAUNCH_ID) ?: "" - - fun NavGraphBuilder.homeScreen( - onNavigateToDetails: (launchId: String) -> Unit, - onNavigateToBookmarks: () -> Unit - ) { - composable( - HOME_ROUTE - ) { - ListOfLaunchesScreen(navigateToDetails = { launchId -> - onNavigateToDetails(launchId) - }, navigateToBookmarks = { - onNavigateToBookmarks() - }) - } - } - - fun NavGraphBuilder.bookmarkScreen( - onNavigateToDetails: (launchId: String) -> Unit, - onBackAction: () -> Unit - ) { - composable( - BOOKMARK_ROUTE - ) { - BookmarkedLaunchesScreen( - navigateToDetails = { launchId -> - onNavigateToDetails(launchId) - }, - navigateBack = onBackAction - ) - } - } - - fun NavGraphBuilder.detailsScreen(onBackAction: () -> Unit) { - composable( - DETAILS_ROUTE - ) { backStackEntry -> - val id = backStackEntry.getArgLaunchId() - if (id.isNotEmpty()) { - LaunchDetailScreen(launchId = id, onBackAction = onBackAction) - } - } - } - - fun NavController.navigateToBookmarks() { - this.navigate(BOOKMARK_ROUTE) - } - - fun NavController.navigateToLaunchDetail(launchId: String) { - this.navigate(buildDetailsRouteWithLaunchId(launchId)) - } - - fun NavController.navigateBack() { - this.popBackStack() - } - - private fun customFadeIn() = fadeIn( - animationSpec = tween( - 300, - easing = LinearEasing - ) - ) - - private fun customFadeOut() = fadeOut( - animationSpec = tween( - 300, - easing = LinearEasing - ) - ) -} +@Serializable +data class RouteDetails(val launchId: String) diff --git a/presentation/src/test/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModelTest.kt b/presentation/src/test/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModelTest.kt index cc975fd..7acbbeb 100644 --- a/presentation/src/test/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModelTest.kt +++ b/presentation/src/test/java/com/nisrulz/example/spacexapi/presentation/features/listoflaunches/ListOfLaunchesViewModelTest.kt @@ -4,7 +4,6 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import com.nisrulz.example.spacexapi.analytics.InUseAnalytics import com.nisrulz.example.spacexapi.analytics.contract.AnalyticsEvent -import com.nisrulz.example.spacexapi.domain.usecase.GetAllBookmarkedLaunches import com.nisrulz.example.spacexapi.domain.usecase.GetAllLaunches import com.nisrulz.example.spacexapi.domain.usecase.ToggleBookmarkLaunchInfo import com.nisrulz.example.spacexapi.logger.InUseLoggers @@ -29,7 +28,6 @@ class ListOfLaunchesViewModelTest { private lateinit var sut: ListOfLaunchesViewModel private lateinit var getAllLaunches: GetAllLaunches private lateinit var bookmarkLaunchInfo: ToggleBookmarkLaunchInfo - private lateinit var getAllBookmarkedLaunches: GetAllBookmarkedLaunches private lateinit var logger: InUseLoggers private lateinit var analytics: InUseAnalytics @@ -37,7 +35,6 @@ class ListOfLaunchesViewModelTest { fun setup() { getAllLaunches = mockk() bookmarkLaunchInfo = mockk() - getAllBookmarkedLaunches = mockk() logger = mockk { every { log(any()) } just runs @@ -50,7 +47,6 @@ class ListOfLaunchesViewModelTest { coroutineDispatcher = testDispatcher, getAllLaunches = getAllLaunches, bookmarkLaunchInfo = bookmarkLaunchInfo, - getAllBookmarkedLaunches = getAllBookmarkedLaunches, logger = logger, analytics = analytics )