Initial commit

This commit is contained in:
2025-09-22 19:25:32 -03:00
parent dcd261a2b3
commit e0062445b6
122 changed files with 4728 additions and 19 deletions

View 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
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -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),
)
}