Initial commit
This commit is contained in:
26
core/navigation/build.gradle.kts
Normal file
26
core/navigation/build.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
group = "dev.carlosmartino.navigation"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinDevKit)
|
||||
alias(libs.plugins.composeDevKit)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
api(libs.compose.multiplatform.navigation)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.designsystem)
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
api(libs.androidx.compose.navigation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
publicResClass = true
|
||||
packageOfResClass = "dev.carlosmartino.navigation.resources"
|
||||
generateResClass = always
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cl.homelogic.platform.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavDeepLink
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import cl.homelogic.platform.common.logging.Trace
|
||||
import cl.homelogic.platform.common.logging.TreeRoots
|
||||
|
||||
inline fun <reified T : NavigationDestination> NavGraphBuilder.createRoute(
|
||||
transition: NavigationTransition = NavigationTransition.FLOW,
|
||||
deepLinks: List<NavDeepLink> = listOf(),
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
) {
|
||||
composable<T>(
|
||||
// enterTransition = {
|
||||
// when (transition) {
|
||||
// NavigationTransition.FLOW -> slideInHorizontally()
|
||||
// NavigationTransition.SINGLE -> NavigationAnimations.verticalEnter
|
||||
// }
|
||||
// },
|
||||
// exitTransition = {
|
||||
// when (transition) {
|
||||
// NavigationTransition.FLOW -> slideOutHorizontally()
|
||||
// NavigationTransition.SINGLE -> NavigationAnimations.verticalExit
|
||||
// }
|
||||
// },
|
||||
// popEnterTransition = {
|
||||
// when (transition) {
|
||||
// NavigationTransition.FLOW -> NavigationAnimations.horizontalPopEnter
|
||||
// NavigationTransition.SINGLE -> NavigationAnimations.verticalPopEnter
|
||||
// }
|
||||
// },
|
||||
// popExitTransition = {
|
||||
// when (transition) {
|
||||
// NavigationTransition.FLOW -> slideOutHorizontally()
|
||||
// NavigationTransition.SINGLE -> NavigationAnimations.verticalPopExit
|
||||
// }
|
||||
// },
|
||||
deepLinks = deepLinks,
|
||||
) { backStackEntry ->
|
||||
val route: T = remember { backStackEntry.toRoute() }
|
||||
val isFirstComposition = remember { true }
|
||||
if (isFirstComposition) {
|
||||
Trace.i(
|
||||
TreeRoots.Navigation,
|
||||
"Route created: ${
|
||||
route::class
|
||||
.qualifiedName
|
||||
?.split(".")
|
||||
?.takeLast(2)
|
||||
?.joinToString(".") ?: route::class.simpleName
|
||||
}",
|
||||
)
|
||||
content(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cl.homelogic.platform.navigation
|
||||
|
||||
import androidx.navigation.NavUri
|
||||
import cl.homelogic.platform.common.logging.Trace
|
||||
import cl.homelogic.platform.common.logging.TreeRoots
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
sealed class NavigationCommand {
|
||||
data class Route(
|
||||
val destination: NavigationDestination,
|
||||
) : NavigationCommand()
|
||||
|
||||
data class DeepLink(
|
||||
val uri: NavUri,
|
||||
) : NavigationCommand()
|
||||
|
||||
data object PopBack : NavigationCommand()
|
||||
|
||||
data class PopUpTo(
|
||||
val destination: NavigationDestination,
|
||||
val inclusive: Boolean = false,
|
||||
) : NavigationCommand()
|
||||
}
|
||||
|
||||
class NavigationManager {
|
||||
private val _navigationEvents = MutableSharedFlow<NavigationCommand>()
|
||||
internal val navigationEvents = _navigationEvents.asSharedFlow()
|
||||
|
||||
private val throttleWindow = 100.milliseconds
|
||||
private var lastNavigationTime: Instant = Instant.DISTANT_PAST
|
||||
|
||||
suspend fun navigate(destination: NavigationDestination) {
|
||||
if (shouldThrottleNavigation()) {
|
||||
Trace.d(TreeRoots.Navigation, "Navigation throttled: ${destination::class}")
|
||||
return
|
||||
}
|
||||
|
||||
Trace.d(
|
||||
TreeRoots.Navigation,
|
||||
"Navigating to: ${
|
||||
destination::class
|
||||
.qualifiedName
|
||||
?.split(".")
|
||||
?.takeLast(2)
|
||||
?.joinToString(".") ?: destination::class.simpleName
|
||||
}",
|
||||
)
|
||||
updateLastNavigationTime()
|
||||
_navigationEvents.emit(NavigationCommand.Route(destination))
|
||||
}
|
||||
|
||||
suspend fun navigate(deepLink: NavUri) {
|
||||
if (shouldThrottleNavigation()) {
|
||||
Trace.d(TreeRoots.Navigation, "DeepLink navigation throttled for: $deepLink")
|
||||
return
|
||||
}
|
||||
|
||||
Trace.d(TreeRoots.Navigation, "Navigating to deeplink: $deepLink")
|
||||
updateLastNavigationTime()
|
||||
_navigationEvents.emit(NavigationCommand.DeepLink(deepLink))
|
||||
}
|
||||
|
||||
suspend fun popBackStack() {
|
||||
if (shouldThrottleNavigation()) {
|
||||
Trace.d(TreeRoots.Navigation, "PopBackStack throttled")
|
||||
return
|
||||
}
|
||||
|
||||
Trace.d(TreeRoots.Navigation, "PopBackStack triggered")
|
||||
updateLastNavigationTime()
|
||||
_navigationEvents.emit(NavigationCommand.PopBack)
|
||||
}
|
||||
|
||||
suspend fun popUpTo(
|
||||
destination: NavigationDestination,
|
||||
inclusive: Boolean,
|
||||
) {
|
||||
if (shouldThrottleNavigation()) {
|
||||
Trace.d(TreeRoots.Navigation, "PopBackStack throttled")
|
||||
return
|
||||
}
|
||||
|
||||
Trace.d(TreeRoots.Navigation, "PopBackStack triggered")
|
||||
updateLastNavigationTime()
|
||||
_navigationEvents.emit(NavigationCommand.PopUpTo(destination, inclusive))
|
||||
}
|
||||
|
||||
private fun shouldThrottleNavigation(): Boolean {
|
||||
val currentTime = Clock.System.now()
|
||||
val lastNavigation = lastNavigationTime
|
||||
return (currentTime - lastNavigation) < throttleWindow
|
||||
}
|
||||
|
||||
private fun updateLastNavigationTime() {
|
||||
lastNavigationTime = Clock.System.now()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cl.homelogic.platform.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
interface NavigationModule {
|
||||
fun navigationGraph(
|
||||
navGraphBuilder: NavGraphBuilder,
|
||||
navController: NavHostController,
|
||||
)
|
||||
}
|
||||
|
||||
interface NavigationDestination
|
||||
@@ -0,0 +1,65 @@
|
||||
package cl.homelogic.platform.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavUri
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.moriatsushi.insetsx.statusBars
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
@Composable
|
||||
fun NavigationSystem(
|
||||
navigationManager: NavigationManager,
|
||||
deeplinkStream: MutableSharedFlow<String>? = null,
|
||||
startDestination: NavigationDestination,
|
||||
navigationModules: List<NavigationModule>,
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
LaunchedEffect(navController) {
|
||||
deeplinkStream?.collect {
|
||||
navController.navigate(NavUri(it))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(navController) {
|
||||
navigationManager.navigationEvents.collect { command ->
|
||||
when (command) {
|
||||
is NavigationCommand.Route -> navController.navigate(command.destination)
|
||||
is NavigationCommand.DeepLink -> navController.navigate(command.uri)
|
||||
is NavigationCommand.PopBack -> navController.popBackStack()
|
||||
is NavigationCommand.PopUpTo -> navController.popBackStack(
|
||||
command.destination,
|
||||
command.inclusive,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(),
|
||||
),
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
) {
|
||||
navigationModules.forEach { module ->
|
||||
module.navigationGraph(this, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package cl.homelogic.platform.navigation
|
||||
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
|
||||
enum class NavigationTransition {
|
||||
FLOW,
|
||||
SINGLE,
|
||||
}
|
||||
|
||||
object NavigationAnimations {
|
||||
private const val ANIMATION_DURATION = 200
|
||||
private const val OVERLAY_SCALE_REDUCED = 0.95f
|
||||
private const val OVERLAY_OPACITY = 0.3f
|
||||
|
||||
val horizontalEnter: EnterTransition = slideInHorizontally(
|
||||
initialOffsetX = { fullWidth -> fullWidth },
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + fadeIn(
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val horizontalExit: ExitTransition = fadeOut(
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + scaleOut(
|
||||
targetScale = OVERLAY_SCALE_REDUCED,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val horizontalPopEnter: EnterTransition = fadeIn(
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + scaleIn(
|
||||
initialScale = OVERLAY_SCALE_REDUCED,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val horizontalPopExit: ExitTransition = slideOutHorizontally(
|
||||
targetOffsetX = { fullWidth -> fullWidth },
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + fadeOut(
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
// For vertical modal transitions
|
||||
val verticalEnter: EnterTransition = slideInVertically(
|
||||
initialOffsetY = { fullHeight -> fullHeight },
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val verticalExit: ExitTransition = fadeOut(
|
||||
targetAlpha = OVERLAY_OPACITY,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + scaleOut(
|
||||
targetScale = OVERLAY_SCALE_REDUCED,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val verticalPopEnter: EnterTransition = fadeIn(
|
||||
initialAlpha = OVERLAY_OPACITY,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
) + scaleIn(
|
||||
initialScale = OVERLAY_SCALE_REDUCED,
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
val verticalPopExit: ExitTransition = slideOutVertically(
|
||||
targetOffsetY = { fullHeight -> fullHeight },
|
||||
animationSpec = tween(ANIMATION_DURATION),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user