From c05c3c371cdb44f3d6f3380fa6cfefc2f852e219 Mon Sep 17 00:00:00 2001 From: Nishant Srivastava Date: Sat, 22 Jun 2024 11:04:04 +0200 Subject: [PATCH 1/4] create compose navigation convention plugin --- build-logic/convention/build.gradle.kts | 6 ++++++ ...AndroidComposeNavigationConventionPlugin.kt | 18 ++++++++++++++++++ .../ktx/AndroidComposeProjectExtensions.kt | 11 +++++++++++ gradle/libs.versions.toml | 3 ++- 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 build-logic/convention/src/main/kotlin/com/nisrulz/example/spacexapi/AndroidComposeNavigationConventionPlugin.kt 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" } From bde2f940831be2a7ef1984fa821c9d564c5f1936 Mon Sep 17 00:00:00 2001 From: Nishant Srivastava Date: Sun, 23 Jun 2024 01:08:20 +0200 Subject: [PATCH 2/4] wire in the compose navigation convention plugin in presentation module + type safe nav --- presentation/build.gradle.kts | 1 + .../presentation/navigation/AppNavigation.kt | 66 +++++------ .../navigation/NavigationRoute.kt | 105 ++---------------- 3 files changed, 42 insertions(+), 130 deletions(-) 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/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) From c0a2daf182cbeabef48932377bc024fb322813cd Mon Sep 17 00:00:00 2001 From: Nishant Srivastava Date: Sun, 23 Jun 2024 01:08:25 +0200 Subject: [PATCH 3/4] code cleanup --- .../bookmarkedlaunches/BookmarkedLaunchesScreen.kt | 10 ++++------ .../features/listoflaunches/ListOfLaunchesViewModel.kt | 10 +++------- .../listoflaunches/ListOfLaunchesViewModelTest.kt | 4 ---- 3 files changed, 7 insertions(+), 17 deletions(-) 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/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 ) From cdcc3de138a2aad861602605b876ded1f887349f Mon Sep 17 00:00:00 2001 From: Nishant Srivastava Date: Sun, 23 Jun 2024 01:11:04 +0200 Subject: [PATCH 4/4] updated readme --- Readme.md | 1 + 1 file changed, 1 insertion(+) 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