Skip to content

Commit 5f0d06b

Browse files
committed
[#610] [Part 2] Update navigation library and refactor to follow unidirectional data flow for template compose
1 parent d220269 commit 5f0d06b

File tree

12 files changed

+191
-75
lines changed

12 files changed

+191
-75
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package co.nimblehq.template.compose.extensions
2+
3+
import androidx.navigation.NavHostController
4+
import androidx.navigation.NavOptionsBuilder
5+
import co.nimblehq.template.compose.ui.base.BaseAppDestination
6+
import kotlin.collections.component1
7+
import kotlin.collections.component2
8+
9+
/**
10+
* Use this extension or [navigate(BaseAppDestination.Up())] to prevent duplicated navigation events
11+
*/
12+
fun NavHostController.navigateAppDestinationUp() {
13+
navigateTo(BaseAppDestination.Up())
14+
}
15+
16+
/**
17+
* TODO Create new class extend NavHostController then move the related codes to that class
18+
*/
19+
private const val IntervalInMillis: Long = 1000L
20+
private var lastNavigationEventExecutedTimeInMillis: Long = 0L
21+
22+
/**
23+
* Use this extension to prevent duplicated navigation events with the same destination in a short time
24+
*/
25+
private fun NavHostController.throttleNavigation(
26+
appDestination: BaseAppDestination,
27+
onNavigate: () -> Unit,
28+
) {
29+
val currentTime = System.currentTimeMillis()
30+
if (currentBackStackEntry?.destination?.route == appDestination.route
31+
&& (currentTime - lastNavigationEventExecutedTimeInMillis < IntervalInMillis)
32+
) {
33+
return
34+
}
35+
lastNavigationEventExecutedTimeInMillis = currentTime
36+
37+
onNavigate()
38+
}
39+
40+
/**
41+
* Navigate to provided [BaseAppDestination]
42+
* Caution to use this method. This method use savedStateHandle to store the Parcelable data.
43+
* When previousBackstackEntry is popped out from navigation stack, savedStateHandle will return null and cannot retrieve data.
44+
* eg.Login -> Home, the Login screen will be popped from the back-stack on logging in successfully.
45+
*/
46+
fun <T : BaseAppDestination> NavHostController.navigateTo(
47+
appDestination: T,
48+
builder: (NavOptionsBuilder.() -> Unit)? = null,
49+
) = throttleNavigation(appDestination) {
50+
when (appDestination) {
51+
is BaseAppDestination.Up -> {
52+
appDestination.results.forEach { (key, value) ->
53+
previousBackStackEntry?.savedStateHandle?.set(key, value)
54+
}
55+
navigateUp()
56+
}
57+
else -> {
58+
appDestination.parcelableArgument?.let { (key, value) ->
59+
currentBackStackEntry?.savedStateHandle?.set(key, value)
60+
}
61+
navigate(route = appDestination.destination) {
62+
if (builder != null) {
63+
builder()
64+
}
65+
}
66+
}
67+
}
68+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package co.nimblehq.template.compose.extensions
2+
3+
import androidx.compose.animation.AnimatedContentScope
4+
import androidx.compose.animation.AnimatedContentTransitionScope
5+
import androidx.compose.animation.EnterTransition
6+
import androidx.compose.animation.ExitTransition
7+
import androidx.compose.animation.core.tween
8+
import androidx.compose.runtime.Composable
9+
import androidx.navigation.NavBackStackEntry
10+
import androidx.navigation.NavDeepLink
11+
import androidx.navigation.NavGraphBuilder
12+
import androidx.navigation.compose.composable
13+
import co.nimblehq.template.compose.ui.base.BaseAppDestination
14+
15+
private const val NavAnimationDurationInMillis = 300
16+
17+
fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInLeftTransition() =
18+
slideIntoContainer(
19+
towards = AnimatedContentTransitionScope.SlideDirection.Start,
20+
animationSpec = tween(NavAnimationDurationInMillis)
21+
)
22+
23+
fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutLeftTransition() =
24+
slideOutOfContainer(
25+
towards = AnimatedContentTransitionScope.SlideDirection.Start,
26+
animationSpec = tween(NavAnimationDurationInMillis)
27+
)
28+
29+
fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInRightTransition() =
30+
slideIntoContainer(
31+
towards = AnimatedContentTransitionScope.SlideDirection.End,
32+
animationSpec = tween(NavAnimationDurationInMillis)
33+
)
34+
35+
fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutRightTransition() =
36+
slideOutOfContainer(
37+
towards = AnimatedContentTransitionScope.SlideDirection.End,
38+
animationSpec = tween(NavAnimationDurationInMillis)
39+
)
40+
41+
fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInUpTransition() =
42+
slideIntoContainer(
43+
towards = AnimatedContentTransitionScope.SlideDirection.Up,
44+
animationSpec = tween(NavAnimationDurationInMillis)
45+
)
46+
47+
fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutDownTransition() =
48+
slideOutOfContainer(
49+
towards = AnimatedContentTransitionScope.SlideDirection.Down,
50+
animationSpec = tween(NavAnimationDurationInMillis)
51+
)
52+
53+
fun NavGraphBuilder.composable(
54+
destination: BaseAppDestination,
55+
deepLinks: List<NavDeepLink> = emptyList(),
56+
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
57+
enterSlideInLeftTransition()
58+
},
59+
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
60+
exitSlideOutLeftTransition()
61+
},
62+
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
63+
enterSlideInRightTransition()
64+
},
65+
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
66+
exitSlideOutRightTransition()
67+
},
68+
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
69+
) {
70+
composable(
71+
route = destination.route,
72+
arguments = destination.arguments,
73+
deepLinks = deepLinks,
74+
enterTransition = enterTransition,
75+
exitTransition = exitTransition,
76+
popEnterTransition = popEnterTransition,
77+
popExitTransition = popExitTransition,
78+
content = content
79+
)
80+
}
81+
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package co.nimblehq.template.compose.ui
22

3-
import co.nimblehq.template.compose.ui.base.BaseDestination
3+
import co.nimblehq.template.compose.ui.base.BaseAppDestination
44

55
sealed class AppDestination {
66

7-
object RootNavGraph : BaseDestination("rootNavGraph")
7+
object RootNavGraph : BaseAppDestination("rootNavGraph")
88

9-
object MainNavGraph : BaseDestination("mainNavGraph")
9+
object MainNavGraph : BaseAppDestination("mainNavGraph")
1010
}
Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
package co.nimblehq.template.compose.ui
22

33
import androidx.compose.runtime.Composable
4-
import androidx.navigation.NavBackStackEntry
5-
import androidx.navigation.NavGraphBuilder
64
import androidx.navigation.NavHostController
75
import androidx.navigation.compose.NavHost
8-
import androidx.navigation.compose.composable
9-
import androidx.navigation.navDeepLink
10-
import co.nimblehq.template.compose.ui.base.BaseDestination
116
import co.nimblehq.template.compose.ui.screens.main.mainNavGraph
127

138
@Composable
@@ -22,31 +17,3 @@ fun AppNavGraph(
2217
mainNavGraph(navController = navController)
2318
}
2419
}
25-
26-
fun NavGraphBuilder.composable(
27-
destination: BaseDestination,
28-
content: @Composable (NavBackStackEntry) -> Unit,
29-
) {
30-
composable(
31-
route = destination.route,
32-
arguments = destination.arguments,
33-
deepLinks = destination.deepLinks.map {
34-
navDeepLink {
35-
uriPattern = it
36-
}
37-
},
38-
content = content
39-
)
40-
}
41-
42-
fun NavHostController.navigate(destination: BaseDestination) {
43-
when (destination) {
44-
is BaseDestination.Up -> {
45-
destination.results.forEach { (key, value) ->
46-
previousBackStackEntry?.savedStateHandle?.set(key, value)
47-
}
48-
navigateUp()
49-
}
50-
else -> navigate(route = destination.destination)
51-
}
52-
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package co.nimblehq.template.compose.ui.base
2+
3+
import androidx.navigation.NamedNavArgument
4+
5+
const val KeyResultOk = "keyResultOk"
6+
7+
/**
8+
* Use "class" over "object" for destinations with [parcelableArgument] usage or a [navArgument] with [defaultValue] set
9+
* to reset destination nav arguments.
10+
*/
11+
abstract class BaseAppDestination(val route: String = "") {
12+
13+
open val arguments: List<NamedNavArgument> = emptyList()
14+
15+
open val deepLinks: List<String> = listOf(
16+
"https://android.nimblehq.co/$route",
17+
"android://$route",
18+
)
19+
20+
open var destination: String = route
21+
22+
open var parcelableArgument: Pair<String, Any?>? = null
23+
24+
data class Up(
25+
val results: HashMap<String, Any> = hashMapOf(),
26+
) : BaseAppDestination() {
27+
28+
fun put(key: String, value: Any) = apply {
29+
results[key] = value
30+
}
31+
}
32+
}

template-compose/app/src/main/java/co/nimblehq/template/compose/ui/base/BaseDestination.kt

Lines changed: 0 additions & 19 deletions
This file was deleted.

template-compose/app/src/main/java/co/nimblehq/template/compose/ui/base/BaseViewModel.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ abstract class BaseViewModel : ViewModel() {
1818
protected val _error = MutableSharedFlow<Throwable>()
1919
val error = _error.asSharedFlow()
2020

21-
protected val _navigator = MutableSharedFlow<BaseDestination>()
22-
val navigator = _navigator.asSharedFlow()
23-
2421
/**
2522
* To show loading manually, should call `hideLoading` after
2623
*/
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package co.nimblehq.template.compose.ui.screens.main
22

3-
import co.nimblehq.template.compose.ui.base.BaseDestination
3+
import co.nimblehq.template.compose.ui.base.BaseAppDestination
44

55
sealed class MainDestination {
66

7-
object Home : BaseDestination("home")
7+
object Home : BaseAppDestination("home")
88
}

template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/MainNavGraph.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package co.nimblehq.template.compose.ui.screens.main
33
import androidx.navigation.NavGraphBuilder
44
import androidx.navigation.NavHostController
55
import androidx.navigation.navigation
6+
import co.nimblehq.template.compose.extensions.composable
67
import co.nimblehq.template.compose.ui.AppDestination
7-
import co.nimblehq.template.compose.ui.composable
8-
import co.nimblehq.template.compose.ui.navigate
98
import co.nimblehq.template.compose.ui.screens.main.home.HomeScreen
109

1110
fun NavGraphBuilder.mainNavGraph(
@@ -16,9 +15,7 @@ fun NavGraphBuilder.mainNavGraph(
1615
startDestination = MainDestination.Home.destination
1716
) {
1817
composable(MainDestination.Home) {
19-
HomeScreen(
20-
navigator = { destination -> navController.navigate(destination) }
21-
)
18+
HomeScreen()
2219
}
2320
}
2421
}

template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeScreen.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
1717
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1818
import co.nimblehq.template.compose.R
1919
import co.nimblehq.template.compose.extensions.collectAsEffect
20-
import co.nimblehq.template.compose.ui.base.BaseDestination
2120
import co.nimblehq.template.compose.ui.base.BaseScreen
2221
import co.nimblehq.template.compose.ui.models.UiModel
2322
import co.nimblehq.template.compose.ui.showToast
@@ -28,11 +27,9 @@ import timber.log.Timber
2827
@Composable
2928
fun HomeScreen(
3029
viewModel: HomeViewModel = hiltViewModel(),
31-
navigator: (destination: BaseDestination) -> Unit,
3230
) = BaseScreen {
3331
val context = LocalContext.current
3432
viewModel.error.collectAsEffect { e -> e.showToast(context) }
35-
viewModel.navigator.collectAsEffect { destination -> navigator(destination) }
3633

3734
val uiModels: List<UiModel> by viewModel.uiModels.collectAsStateWithLifecycle()
3835

0 commit comments

Comments
 (0)