Skip to content

[#610] [Part 2] Update navigation library and refactor template to expose event callback to navigate #612

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

Open
wants to merge 1 commit into
base: chore/610-part-1-update-navigation-library-and-refactor-to-follow-unidirectional-data-flow
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package co.nimblehq.template.compose.extensions

import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import co.nimblehq.template.compose.ui.base.BaseAppDestination
import kotlin.collections.component1
import kotlin.collections.component2

/**
* Use this extension or [navigate(BaseAppDestination.Up())] to prevent duplicated navigation events
*/
fun NavHostController.navigateAppDestinationUp() {
navigateTo(BaseAppDestination.Up())
}

private const val IntervalInMillis: Long = 1000L
private var lastNavigationEventExecutedTimeInMillis: Long = 0L

/**
* Use this extension to prevent duplicated navigation events with the same destination in a short time
*/
private fun NavHostController.throttleNavigation(
appDestination: BaseAppDestination,
onNavigate: () -> Unit,
) {
val currentTime = System.currentTimeMillis()
if (currentBackStackEntry?.destination?.route == appDestination.route
&& (currentTime - lastNavigationEventExecutedTimeInMillis < IntervalInMillis)
) {
return
}
lastNavigationEventExecutedTimeInMillis = currentTime

onNavigate()
}

/**
* Navigate to provided [BaseAppDestination]
* 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 <T : BaseAppDestination> NavHostController.navigateTo(
appDestination: T,
builder: (NavOptionsBuilder.() -> Unit)? = null,
) = throttleNavigation(appDestination) {
when (appDestination) {
is BaseAppDestination.Up -> {
appDestination.results.forEach { (key, value) ->
previousBackStackEntry?.savedStateHandle?.set(key, value)
}
navigateUp()
}
else -> {
appDestination.parcelableArgument?.let { (key, value) ->
currentBackStackEntry?.savedStateHandle?.set(key, value)
}
navigate(route = appDestination.destination) {
if (builder != null) {
builder()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package co.nimblehq.template.compose.extensions

import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import co.nimblehq.template.compose.ui.base.BaseAppDestination

private const val NavAnimationDurationInMillis = 300

fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInLeftTransition() =
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutLeftTransition() =
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInRightTransition() =
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.End,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutRightTransition() =
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.End,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun AnimatedContentTransitionScope<NavBackStackEntry>.enterSlideInUpTransition() =
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Up,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun AnimatedContentTransitionScope<NavBackStackEntry>.exitSlideOutDownTransition() =
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Down,
animationSpec = tween(NavAnimationDurationInMillis)
)

fun NavGraphBuilder.composable(
destination: BaseAppDestination,
deepLinks: List<NavDeepLink> = emptyList(),
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
enterSlideInLeftTransition()
},
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
exitSlideOutLeftTransition()
},
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
enterSlideInRightTransition()
},
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
exitSlideOutRightTransition()
},
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
composable(
route = destination.route,
arguments = destination.arguments,
deepLinks = deepLinks,
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
content = content
)
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package co.nimblehq.template.compose.ui

import co.nimblehq.template.compose.ui.base.BaseDestination
import co.nimblehq.template.compose.ui.base.BaseAppDestination

sealed class AppDestination {

object RootNavGraph : BaseDestination("rootNavGraph")
object RootNavGraph : BaseAppDestination("rootNavGraph")

object MainNavGraph : BaseDestination("mainNavGraph")
object MainNavGraph : BaseAppDestination("mainNavGraph")
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package co.nimblehq.template.compose.ui

import androidx.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import co.nimblehq.template.compose.ui.base.BaseDestination
import co.nimblehq.template.compose.ui.screens.main.mainNavGraph

@Composable
Expand All @@ -22,31 +17,3 @@ fun AppNavGraph(
mainNavGraph(navController = navController)
}
}

fun NavGraphBuilder.composable(
destination: BaseDestination,
content: @Composable (NavBackStackEntry) -> Unit,
) {
composable(
route = destination.route,
arguments = destination.arguments,
deepLinks = destination.deepLinks.map {
navDeepLink {
uriPattern = it
}
},
content = content
)
}

fun NavHostController.navigate(destination: BaseDestination) {
when (destination) {
is BaseDestination.Up -> {
destination.results.forEach { (key, value) ->
previousBackStackEntry?.savedStateHandle?.set(key, value)
}
navigateUp()
}
else -> navigate(route = destination.destination)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package co.nimblehq.template.compose.ui.base

import androidx.navigation.NamedNavArgument

const val KeyResultOk = "keyResultOk"

/**
* Use "class" over "object" for destinations with [parcelableArgument] usage or a [navArgument] with [defaultValue] set
* to reset destination nav arguments.
*/
abstract class BaseAppDestination(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?>? = null

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

fun put(key: String, value: Any) = apply {
results[key] = value
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ abstract class BaseViewModel : ViewModel() {
protected val _error = MutableSharedFlow<Throwable>()
val error = _error.asSharedFlow()

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

/**
* To show loading manually, should call `hideLoading` after
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package co.nimblehq.template.compose.ui.screens.main

import co.nimblehq.template.compose.ui.base.BaseDestination
import co.nimblehq.template.compose.ui.base.BaseAppDestination

sealed class MainDestination {

object Home : BaseDestination("home")
object Home : BaseAppDestination("home")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package co.nimblehq.template.compose.ui.screens.main
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.navigation
import co.nimblehq.template.compose.extensions.composable
import co.nimblehq.template.compose.ui.AppDestination
import co.nimblehq.template.compose.ui.composable
import co.nimblehq.template.compose.ui.navigate
import co.nimblehq.template.compose.ui.screens.main.home.HomeScreen

fun NavGraphBuilder.mainNavGraph(
Expand All @@ -16,9 +15,7 @@ fun NavGraphBuilder.mainNavGraph(
startDestination = MainDestination.Home.destination
) {
composable(MainDestination.Home) {
HomeScreen(
navigator = { destination -> navController.navigate(destination) }
)
HomeScreen()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.nimblehq.template.compose.R
import co.nimblehq.template.compose.extensions.collectAsEffect
import co.nimblehq.template.compose.ui.base.BaseDestination
import co.nimblehq.template.compose.ui.base.BaseScreen
import co.nimblehq.template.compose.ui.models.UiModel
import co.nimblehq.template.compose.ui.showToast
Expand All @@ -28,11 +27,9 @@ import timber.log.Timber
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
navigator: (destination: BaseDestination) -> Unit,
) = BaseScreen {
val context = LocalContext.current
viewModel.error.collectAsEffect { e -> e.showToast(context) }
viewModel.navigator.collectAsEffect { destination -> navigator(destination) }

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import co.nimblehq.template.compose.R
import co.nimblehq.template.compose.domain.usecases.UseCase
import co.nimblehq.template.compose.test.MockUtil
import co.nimblehq.template.compose.ui.base.BaseDestination
import co.nimblehq.template.compose.ui.screens.BaseScreenTest
import co.nimblehq.template.compose.ui.screens.MainActivity
import co.nimblehq.template.compose.ui.theme.ComposeTheme
Expand All @@ -16,7 +15,6 @@ import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.*
import org.junit.*
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
Expand All @@ -31,7 +29,6 @@ class HomeScreenTest : BaseScreenTest() {
private val mockUseCase: UseCase = mockk()

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

@Before
fun setUp() {
Expand Down Expand Up @@ -67,7 +64,6 @@ class HomeScreenTest : BaseScreenTest() {
ComposeTheme {
HomeScreen(
viewModel = viewModel,
navigator = { destination -> expectedDestination = destination }
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions template-compose/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ accompanist = "0.30.1"
chucker = "4.0.0"
composeBom = "2025.02.00"
Copy link

@github-actions github-actions bot May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of androidx.compose:compose-bom than 2025.02.00 is available: 2025.04.01

# @kaungkhantsoe Will update in a separate PR
composeNavigation = "2.5.3"
composeNavigation = "2.8.9"
core = "1.15.0"
Copy link

@github-actions github-actions bot May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of androidx.core:core-ktx than 1.15.0 is available: 1.16.0

datastore = "1.1.2"
Copy link

@github-actions github-actions bot May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of androidx.datastore:datastore-preferences than 1.1.2 is available: 1.1.5

detekt = "1.21.0"
gradle = "8.8.1"
Copy link

@github-actions github-actions bot May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of com.android.application than 8.8.1 is available: 8.9.2. (There is also a newer version of 8.8.𝑥 available, if upgrading to 8.9.2 is difficult: 8.8.2)

Copy link

@github-actions github-actions bot May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ A newer version of com.android.library than 8.8.1 is available: 8.9.2. (There is also a newer version of 8.8.𝑥 available, if upgrading to 8.9.2 is difficult: 8.8.2)

hilt = "2.52"
hilt = "2.53"
hiltNavigation = "1.2.0"
javaxInject = "1"
junit = "4.13.2"
Expand All @@ -24,7 +24,7 @@ kotlinxCoroutines = "1.7.3"
kover = "0.7.3"
ksp = "2.1.0-1.0.29"
lifecycle = "2.8.7"
mockk = "1.13.5"
mockk = "1.13.17"
moshi = "1.15.1"
nimbleCommon = "0.1.2"
okhttp = "4.12.0"
Expand Down
Loading