Skip to content

Instantly share code, notes, and snippets.

@JakubNeukirch
Created October 28, 2024 18:21
Show Gist options
  • Save JakubNeukirch/bbe417e2b71ca4aed05a49e3b9615bfe to your computer and use it in GitHub Desktop.
Save JakubNeukirch/bbe417e2b71ca4aed05a49e3b9615bfe to your computer and use it in GitHub Desktop.
Navigation in Kotlin Multiplatform with Compose
navigationController.navigate(DetailRoute(item))
data class DetailRoute(
private val index: Int
): Route() {
@Composable
override fun Content(controller: NavigationController) {
DetailPage(controller, index)
}
}
/**
* The model that defines what action is taken - NavigationParent will select proper animation based on its type.
*/
sealed class NavigationAction {
abstract val route: Route
data class Idle(override val route: Route) : NavigationAction()
data class Navigate(override val route: Route) : NavigationAction()
data class PopTo(override val route: Route) : NavigationAction()
}
/**
* Controls the backstack
*/
class NavigationController(
initialRoute: Route = Route(),
) {
private var _routeStack = mutableListOf(initialRoute)
private val _navigationAction = MutableStateFlow<NavigationAction>(NavigationAction.Idle(_routeStack.last()))
val navigationAction: StateFlow<NavigationAction> = _navigationAction
fun navigate(route: Route) {
_routeStack += route
_navigationAction.value = NavigationAction.Navigate(route)
}
fun navigateBack() {
if (_routeStack.size > 1) {
_routeStack = _routeStack.dropLast(1).toMutableList()
_navigationAction.value = NavigationAction.PopTo(_routeStack.last())
}
}
fun navigateBackUntil(predicate: (Route) -> Boolean) {
val index = _routeStack.indexOfLast(predicate)
if (index != -1) {
_routeStack = _routeStack.take(index + 1).toMutableList()
_navigationAction.value = NavigationAction.PopTo(_routeStack.last())
}
}
fun replace(route: Route) {
_routeStack = (_routeStack.dropLast(1) + route).toMutableList()
_navigationAction.value = NavigationAction.Navigate(route)
}
fun replaceUntil(predicate: (Route) -> Boolean, route: Route) {
val index = _routeStack.indexOfLast(predicate)
if (index != -1) {
_routeStack = (_routeStack.take(index + 1) + route).toMutableList()
_navigationAction.value = NavigationAction.Navigate(route)
}
}
}
/**
* The composable responsible for laying out the proper route, and animating transitions.
*/
@Composable
fun NavigationParent(
controller: NavigationController = rememberNavigationController(),
) {
val currentAction by controller.navigationAction.collectAsState()
AnimatedContent(
targetState = currentAction,
transitionSpec = {
when(this.targetState) {
is NavigationAction.Idle -> fadeIn() togetherWith fadeOut()
is NavigationAction.Navigate -> navigateToAnimation()
is NavigationAction.PopTo -> popToNavigation()
}
}
) { action ->
action.route.Content(controller)
}
}
private fun <S> AnimatedContentTransitionScope<S>.navigateToAnimation(): ContentTransform {
return slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
) togetherWith slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(300)
)
}
private fun <S> AnimatedContentTransitionScope<S>.popToNavigation(): ContentTransform {
return slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
) togetherWith slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(300)
)
}
/**
* Base `Route` class - you can provide parameters to constructor, and define the screen to be displayed for this route.
*/
open class Route {
@Composable
open fun Content(controller: NavigationController) {
}
override fun toString(): String {
return "${this::class.simpleName}"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment