Skip to content

Commit

Permalink
Merge branch 'feature/define-results-for-up-navigation' into feature/…
Browse files Browse the repository at this point in the history
…define-deeplink-structure
  • Loading branch information
luongvo committed Nov 22, 2023
2 parents f085023 + a1c2fd9 commit 1974e18
Show file tree
Hide file tree
Showing 24 changed files with 196 additions and 146 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.nimblehq.sample.compose.test

import co.nimblehq.sample.compose.domain.model.Model

object MockUtil {

val models = listOf(
Model(
id = 1,
username = "name1",
),
Model(
id = 2,
username = "name2",
),
Model(
id = 3,
username = "name3",
),
)
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package co.nimblehq.sample.compose.ui.screens.home
package co.nimblehq.sample.compose.ui.screens.main.home

import androidx.activity.compose.setContent
import androidx.compose.ui.test.*
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.rule.GrantPermissionRule
import co.nimblehq.sample.compose.domain.model.Model
import co.nimblehq.sample.compose.domain.usecase.*
import co.nimblehq.sample.compose.domain.usecase.GetModelsUseCase
import co.nimblehq.sample.compose.domain.usecase.IsFirstTimeLaunchPreferencesUseCase
import co.nimblehq.sample.compose.domain.usecase.UpdateFirstTimeLaunchPreferencesUseCase
import co.nimblehq.sample.compose.test.MockUtil
import co.nimblehq.sample.compose.test.TestDispatchersProvider
import co.nimblehq.sample.compose.ui.AppDestination
import co.nimblehq.sample.compose.ui.base.BaseDestination
import co.nimblehq.sample.compose.ui.screens.MainActivity
import co.nimblehq.sample.compose.ui.screens.main.MainDestination
import co.nimblehq.sample.compose.ui.theme.ComposeTheme
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import org.junit.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeScreenTest {

Expand All @@ -36,13 +43,11 @@ class HomeScreenTest {
private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase = mockk()

private lateinit var viewModel: HomeViewModel
private var expectedAppDestination: AppDestination? = null
private var expectedDestination: BaseDestination? = null

@Before
fun setUp() {
every { mockGetModelsUseCase() } returns flowOf(
listOf(Model(1), Model(2), Model(3))
)
every { mockGetModelsUseCase() } returns flowOf(MockUtil.models)
every { mockIsFirstTimeLaunchPreferencesUseCase() } returns flowOf(false)

viewModel = HomeViewModel(
Expand All @@ -69,7 +74,7 @@ class HomeScreenTest {
fun when_clicking_on_a_list_item__it_navigates_to_Second_screen() = initComposable {
onNodeWithText("1").performClick()

assertEquals(expectedAppDestination, AppDestination.Second)
assertEquals(expectedDestination, MainDestination.Second)
}

private fun initComposable(
Expand All @@ -79,7 +84,7 @@ class HomeScreenTest {
ComposeTheme {
HomeScreen(
viewModel = viewModel,
navigator = { destination -> expectedAppDestination = destination }
navigator = { destination -> expectedDestination = destination }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package co.nimblehq.sample.compose.extensions

import androidx.lifecycle.SavedStateHandle

fun <T> SavedStateHandle.getAndRemove(key: String): T? {
fun <T> SavedStateHandle.getThenRemove(key: String): T? {
return if (contains(key)) {
val value = get<T>(key)
remove<T>(key)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,10 @@
package co.nimblehq.sample.compose.ui

import androidx.navigation.NamedNavArgument
import androidx.navigation.NavType
import androidx.navigation.navArgument
import co.nimblehq.sample.compose.model.UiModel
import co.nimblehq.sample.compose.ui.base.BaseDestination

const val KeyId = "id"
const val KeyModel = "model"
const val KeyResultOk = "keyResultOk"
sealed class AppDestination {

sealed class AppDestination(val route: String = "") {
object RootNavGraph : BaseDestination("rootNavGraph")

open val arguments: List<NamedNavArgument> = emptyList()

open val deepLinks: List<String> = listOf(
"https://android.nimblehq.co/$route",
"android://$route",
)

open var destination: String = route

open var parcelableArgument: Pair<String, Any?> = "" to null

data class Up(val results: List<Result> = emptyList()) : AppDestination()

object RootNavGraph : AppDestination("rootNavGraph")

object MainNavGraph : AppDestination("mainNavGraph")

object Home : AppDestination("home")

object Second : AppDestination("second/{$KeyId}") {

override val arguments = listOf(
navArgument(KeyId) { type = NavType.StringType }
)

fun createRoute(id: String) = apply {
destination = "second/$id"
}
}

object Third : AppDestination("third") {
fun addParcel(value: UiModel) = apply {
parcelableArgument = KeyModel to value
}
}
object MainNavGraph : BaseDestination("mainNavGraph")
}

data class Result(
val key: String,
val value: Any,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import co.nimblehq.sample.compose.ui.base.BaseDestination
import co.nimblehq.sample.compose.ui.screens.main.mainNavGraph

@Composable
Expand All @@ -23,7 +24,7 @@ fun AppNavGraph(
}

fun NavGraphBuilder.composable(
destination: AppDestination,
destination: BaseDestination,
content: @Composable (NavBackStackEntry) -> Unit,
) {
composable(
Expand All @@ -39,25 +40,24 @@ fun NavGraphBuilder.composable(
}

/**
* Navigate to provided [AppDestination] with a Pair of key value String and Data [parcel]
* Navigate to provided [BaseDestination] with a Pair of key value String and Data [parcel]
* Caution to use this method. This method use savedStateHandle to store the Parcelable data.
* When previousBackstackEntry is popped out from navigation stack, savedStateHandle will return null and cannot retrieve data.
* eg.Login -> Home, the Login screen will be popped from the back-stack on logging in successfully.
*/
fun NavHostController.navigate(appDestination: AppDestination, parcel: Pair<String, Any?>? = null) {
when (appDestination) {
is AppDestination.Up -> {
appDestination.results.forEach { (key, value) ->
fun NavHostController.navigate(destination: BaseDestination, parcel: Pair<String, Any?>? = null) {
when (destination) {
is BaseDestination.Up -> {
destination.results.forEach { (key, value) ->
previousBackStackEntry?.savedStateHandle?.set(key, value)
}
navigateUp()
}

else -> {
parcel?.let { (key, value) ->
currentBackStackEntry?.savedStateHandle?.set(key, value)
}
navigate(route = appDestination.destination)
navigate(route = destination.destination)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package co.nimblehq.sample.compose.ui.base

import androidx.navigation.NamedNavArgument

const val KeyResultOk = "keyResultOk"

abstract class BaseDestination(val route: String = "") {

open val arguments: List<NamedNavArgument> = emptyList()

open val deepLinks: List<String> = listOf(
"https://android.nimblehq.co/$route",
"android://$route",
)

open var destination: String = route

open var parcelableArgument: Pair<String, Any?> = "" to null

data class Up(val results: HashMap<String, Any> = hashMapOf()) : BaseDestination() {

fun addResult(key: String, value: Any) = apply {
results[key] = value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package co.nimblehq.sample.compose.ui.base

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.nimblehq.sample.compose.ui.AppDestination
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.coroutines.CoroutineContext
Expand All @@ -19,7 +18,7 @@ abstract class BaseViewModel : ViewModel() {
protected val _error = MutableSharedFlow<Throwable>()
val error = _error.asSharedFlow()

protected val _navigator = MutableSharedFlow<AppDestination>()
protected val _navigator = MutableSharedFlow<BaseDestination>()
val navigator = _navigator.asSharedFlow()

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package co.nimblehq.sample.compose.ui.screens.main

import androidx.navigation.NavType
import androidx.navigation.navArgument
import co.nimblehq.sample.compose.model.UiModel
import co.nimblehq.sample.compose.ui.base.BaseDestination

const val KeyId = "id"
const val KeyModel = "model"

sealed class MainDestination {

object Home : BaseDestination("home")

object Second : BaseDestination("second/{$KeyId}") {

override val arguments = listOf(
navArgument(KeyId) { type = NavType.StringType }
)

fun createRoute(id: String) = apply {
destination = "second/$id"
}
}

object Third : BaseDestination("third") {
fun addParcel(value: UiModel) = apply {
parcelableArgument = KeyModel to value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package co.nimblehq.sample.compose.ui.screens.main
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.navigation
import co.nimblehq.sample.compose.extensions.getAndRemove
import co.nimblehq.sample.compose.extensions.getThenRemove
import co.nimblehq.sample.compose.model.UiModel
import co.nimblehq.sample.compose.ui.AppDestination
import co.nimblehq.sample.compose.ui.KeyId
import co.nimblehq.sample.compose.ui.KeyModel
import co.nimblehq.sample.compose.ui.KeyResultOk
import co.nimblehq.sample.compose.ui.base.KeyResultOk
import co.nimblehq.sample.compose.ui.composable
import co.nimblehq.sample.compose.ui.navigate
import co.nimblehq.sample.compose.ui.screens.main.home.HomeScreen
Expand All @@ -20,11 +18,11 @@ fun NavGraphBuilder.mainNavGraph(
) {
navigation(
route = AppDestination.MainNavGraph.route,
startDestination = AppDestination.Home.destination
startDestination = MainDestination.Home.destination
) {
composable(destination = AppDestination.Home) { backStackEntry ->
composable(destination = MainDestination.Home) { backStackEntry ->
val isResultOk = backStackEntry.savedStateHandle
.getAndRemove<Boolean>(KeyResultOk) ?: false
.getThenRemove<Boolean>(KeyResultOk) ?: false
HomeScreen(
navigator = { destination ->
navController.navigate(destination, destination.parcelableArgument)
Expand All @@ -33,14 +31,14 @@ fun NavGraphBuilder.mainNavGraph(
)
}

composable(destination = AppDestination.Second) { backStackEntry ->
composable(destination = MainDestination.Second) { backStackEntry ->
SecondScreen(
navigator = { destination -> navController.navigate(destination) },
id = backStackEntry.arguments?.getString(KeyId).orEmpty()
)
}

composable(destination = AppDestination.Third) {
composable(destination = MainDestination.Third) {
ThirdScreen(
navigator = { destination -> navController.navigate(destination) },
model = navController.previousBackStackEntry?.savedStateHandle?.get<UiModel>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import co.nimblehq.sample.compose.extensions.collectAsEffect
import co.nimblehq.sample.compose.extensions.showToast
import co.nimblehq.sample.compose.lib.IsLoading
import co.nimblehq.sample.compose.model.UiModel
import co.nimblehq.sample.compose.ui.AppDestination
import co.nimblehq.sample.compose.ui.base.BaseDestination
import co.nimblehq.sample.compose.ui.common.AppBar
import co.nimblehq.sample.compose.ui.showToast
import co.nimblehq.sample.compose.ui.theme.ComposeTheme
Expand All @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.*
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
navigator: (destination: AppDestination) -> Unit,
navigator: (destination: BaseDestination) -> Unit,
isResultOk: Boolean = false,
) {
val context = LocalContext.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import co.nimblehq.sample.compose.domain.usecase.IsFirstTimeLaunchPreferencesUse
import co.nimblehq.sample.compose.domain.usecase.UpdateFirstTimeLaunchPreferencesUseCase
import co.nimblehq.sample.compose.model.UiModel
import co.nimblehq.sample.compose.model.toUiModel
import co.nimblehq.sample.compose.ui.AppDestination
import co.nimblehq.sample.compose.ui.base.BaseViewModel
import co.nimblehq.sample.compose.ui.screens.main.MainDestination
import co.nimblehq.sample.compose.util.DispatchersProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -60,10 +60,10 @@ class HomeViewModel @Inject constructor(
}

fun navigateToSecond(uiModel: UiModel) {
launch { _navigator.emit(AppDestination.Second.createRoute(uiModel.id)) }
launch { _navigator.emit(MainDestination.Second.createRoute(uiModel.id)) }
}

fun navigateToThird(uiModel: UiModel) {
launch { _navigator.emit(AppDestination.Third.addParcel(uiModel)) }
launch { _navigator.emit(MainDestination.Third.addParcel(uiModel)) }
}
}
Loading

0 comments on commit 1974e18

Please sign in to comment.