Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: Type safe compose navigation #37

Merged
merged 4 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions build-logic/convention/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,11 @@ gradlePlugin {
implementationClass =
"com.nisrulz.example.spacexapi.AndroidComposeConventionPlugin"
}

register("androidComposeNav") {
id = "spacexapi.android.compose.navigation"
implementationClass =
"com.nisrulz.example.spacexapi.AndroidComposeNavigationConventionPlugin"
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.plugin.serialization")
}

configureAndroidComposeNavigation()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,14 @@ internal fun Project.configureAndroidCompose() = configure<LibraryExtension> {
enableStrongSkippingMode = true
}
}

/**
* Configure Compose Navigation options
*/
internal fun Project.configureAndroidComposeNavigation() = configure<LibraryExtension> {

dependencies {
add("implementation", catalogLibrary("navigation-compose"))
add("implementation", catalogLibrary("kotlinx-serialization-json"))
}
}
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
@Composable
fun BookmarkedLaunchesScreen(
viewModel: BookmarkedLaunchesViewModel = hiltViewModel(),
navigateBack: EmptyCallback = {},
onBackAction: EmptyCallback = {},
navigateToDetails: SingleValueCallback<String> = {}
) {
val snackbarHostState = remember { SnackbarHostState() }
Expand All @@ -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)
}
Expand Down Expand Up @@ -63,8 +63,7 @@ fun BookmarkedLaunchesScreen(
viewModel.navigateBack()
}
} else {
BookmarkedLaunchesListComponent(
state = state,
BookmarkedLaunchesListComponent(state = state,
snackbarHostState = snackbarHostState,
navigateToDetails = {
viewModel.navigateToDetails(it)
Expand All @@ -74,8 +73,7 @@ fun BookmarkedLaunchesScreen(
},
navigateBack = {
viewModel.navigateBack()
}
)
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -50,8 +47,7 @@ constructor(

@VisibleForTesting
fun getListOfLaunches() = viewModelScope.launch(coroutineDispatcher) {
getAllLaunches()
.onEach {
getAllLaunches().onEach {
handleListOfLaunches(it)
}.catch {
setError(it.message ?: "Error")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteHome> {
ListOfLaunchesScreen(navigateToDetails = { launchId ->
navController.navigate(RouteDetails(launchId))
}, navigateToBookmarks = {
navController.navigate(RouteBookmark)
})

}

composable<RouteBookmark> {
BookmarkedLaunchesScreen(navigateToDetails = { launchId ->
navController.navigate(RouteDetails(launchId))
}, onBackAction = {
navController.navigate(RouteHome)
})

}

composable<RouteDetails> { backStackEntry ->
val id = backStackEntry.toRoute<RouteDetails>().launchId

LaunchDetailScreen(launchId = id, onBackAction = {
navController.navigate(RouteHome)
})

}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,15 +28,13 @@ 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

@Before
fun setup() {
getAllLaunches = mockk()
bookmarkLaunchInfo = mockk()
getAllBookmarkedLaunches = mockk()

logger = mockk {
every { log(any<String>()) } just runs
Expand All @@ -50,7 +47,6 @@ class ListOfLaunchesViewModelTest {
coroutineDispatcher = testDispatcher,
getAllLaunches = getAllLaunches,
bookmarkLaunchInfo = bookmarkLaunchInfo,
getAllBookmarkedLaunches = getAllBookmarkedLaunches,
logger = logger,
analytics = analytics
)
Expand Down