0.6.0
This commit is contained in:
1
compose/routing/.gitignore
vendored
Normal file
1
compose/routing/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
46
compose/routing/build.gradle.kts
Normal file
46
compose/routing/build.gradle.kts
Normal file
@@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "it.hamy.compose.routing"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xcontext-receivers")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(libs.versions.jvm.get().toInt())
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.animation)
|
||||
|
||||
detektPlugins(libs.detekt.compose)
|
||||
detektPlugins(libs.detekt.formatting)
|
||||
}
|
||||
2
compose/routing/src/main/AndroidManifest.xml
Normal file
2
compose/routing/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,14 @@
|
||||
package it.hamy.compose.routing
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
internal val globalRouteFlow = MutableSharedFlow<Pair<Route, Array<Any?>>>(extraBufferCapacity = 1)
|
||||
|
||||
@Composable
|
||||
fun OnGlobalRoute(block: suspend (Pair<Route, Array<Any?>>) -> Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
globalRouteFlow.collect(block)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package it.hamy.compose.routing
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.saveable.SaverScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@Immutable
|
||||
open class Route internal constructor(val tag: String) {
|
||||
override fun equals(other: Any?) = when {
|
||||
this === other -> true
|
||||
other is Route -> tag == other.tag
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun hashCode() = tag.hashCode()
|
||||
|
||||
object Saver : androidx.compose.runtime.saveable.Saver<Route?, String> {
|
||||
override fun restore(value: String): Route? = value.takeIf(String::isNotEmpty)?.let(::Route)
|
||||
override fun SaverScope.save(value: Route?): String = value?.tag.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class Route0(tag: String) : Route(tag) {
|
||||
context(RouteHandlerScope)
|
||||
@Composable
|
||||
operator fun invoke(content: @Composable () -> Unit) {
|
||||
if (this == route) content()
|
||||
}
|
||||
|
||||
fun global() {
|
||||
globalRouteFlow.tryEmit(this to emptyArray())
|
||||
}
|
||||
|
||||
suspend fun ensureGlobal() {
|
||||
globalRouteFlow.subscriptionCount.filter { it > 0 }.first()
|
||||
globalRouteFlow.emit(this to arrayOf())
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class Route1<P0>(tag: String) : Route(tag) {
|
||||
context(RouteHandlerScope)
|
||||
@Composable
|
||||
operator fun invoke(content: @Composable (P0) -> Unit) {
|
||||
if (this == route) content(parameters[0] as P0)
|
||||
}
|
||||
|
||||
fun global(p0: P0) {
|
||||
globalRouteFlow.tryEmit(this to arrayOf(p0))
|
||||
}
|
||||
|
||||
suspend fun ensureGlobal(p0: P0) {
|
||||
globalRouteFlow.subscriptionCount.filter { it > 0 }.first()
|
||||
globalRouteFlow.emit(this to arrayOf(p0))
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class Route2<P0, P1>(tag: String) : Route(tag) {
|
||||
context(RouteHandlerScope)
|
||||
@Composable
|
||||
operator fun invoke(content: @Composable (P0, P1) -> Unit) {
|
||||
if (this == route) content(parameters[0] as P0, parameters[1] as P1)
|
||||
}
|
||||
|
||||
fun global(p0: P0, p1: P1) {
|
||||
globalRouteFlow.tryEmit(this to arrayOf(p0, p1))
|
||||
}
|
||||
|
||||
suspend fun ensureGlobal(p0: P0, p1: P1) {
|
||||
globalRouteFlow.subscriptionCount.filter { it > 0 }.first()
|
||||
globalRouteFlow.emit(this to arrayOf(p0, p1))
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class Route3<P0, P1, P2>(tag: String) : Route(tag) {
|
||||
context(RouteHandlerScope)
|
||||
@Composable
|
||||
operator fun invoke(content: @Composable (P0, P1, P2) -> Unit) {
|
||||
if (this == route) content(parameters[0] as P0, parameters[1] as P1, parameters[2] as P2)
|
||||
}
|
||||
|
||||
fun global(p0: P0, p1: P1, p2: P2) {
|
||||
globalRouteFlow.tryEmit(this to arrayOf(p0, p1, p2))
|
||||
}
|
||||
|
||||
suspend fun ensureGlobal(p0: P0, p1: P1, p2: P2) {
|
||||
globalRouteFlow.subscriptionCount.filter { it > 0 }.first()
|
||||
globalRouteFlow.emit(this to arrayOf(p0, p1, p2))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package it.hamy.compose.routing
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun RouteHandler(
|
||||
modifier: Modifier = Modifier,
|
||||
listenToGlobalEmitter: Boolean = false,
|
||||
handleBackPress: Boolean = true,
|
||||
transitionSpec: AnimatedContentTransitionScope<RouteHandlerScope>.() -> ContentTransform = {
|
||||
when {
|
||||
isStacking -> defaultStacking
|
||||
isStill -> defaultStill
|
||||
else -> defaultUnstacking
|
||||
}
|
||||
},
|
||||
content: @Composable RouteHandlerScope.() -> Unit
|
||||
) {
|
||||
var route by rememberSaveable(stateSaver = Route.Saver) {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
RouteHandler(
|
||||
route = route,
|
||||
onRouteChanged = { route = it },
|
||||
listenToGlobalEmitter = listenToGlobalEmitter,
|
||||
handleBackPress = handleBackPress,
|
||||
transitionSpec = transitionSpec,
|
||||
modifier = modifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun RouteHandler(
|
||||
route: Route?,
|
||||
onRouteChanged: (Route?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
listenToGlobalEmitter: Boolean = false,
|
||||
handleBackPress: Boolean = true,
|
||||
transitionSpec: AnimatedContentTransitionScope<RouteHandlerScope>.() -> ContentTransform = {
|
||||
when {
|
||||
isStacking -> defaultStacking
|
||||
isStill -> defaultStill
|
||||
else -> defaultUnstacking
|
||||
}
|
||||
},
|
||||
content: @Composable RouteHandlerScope.() -> Unit
|
||||
) {
|
||||
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
|
||||
val parameters = rememberSaveable {
|
||||
arrayOfNulls<Any?>(3)
|
||||
}
|
||||
|
||||
val scope = remember(route) {
|
||||
RouteHandlerScope(
|
||||
route = route,
|
||||
parameters = parameters,
|
||||
push = onRouteChanged,
|
||||
pop = { if (handleBackPress) backDispatcher?.onBackPressed() else onRouteChanged(null) }
|
||||
)
|
||||
}
|
||||
|
||||
if (listenToGlobalEmitter && route == null) {
|
||||
OnGlobalRoute { (newRoute, newParameters) ->
|
||||
newParameters.forEachIndexed(parameters::set)
|
||||
onRouteChanged(newRoute)
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = handleBackPress && route != null) {
|
||||
onRouteChanged(null)
|
||||
}
|
||||
|
||||
updateTransition(targetState = scope, label = null).AnimatedContent(
|
||||
transitionSpec = transitionSpec,
|
||||
contentKey = RouteHandlerScope::route,
|
||||
modifier = modifier
|
||||
) {
|
||||
it.content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package it.hamy.compose.routing
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
class RouteHandlerScope(
|
||||
val route: Route?,
|
||||
val parameters: Array<Any?>,
|
||||
private val push: (Route?) -> Unit,
|
||||
val pop: () -> Unit
|
||||
) {
|
||||
@Composable
|
||||
inline fun NavHost(content: @Composable () -> Unit) {
|
||||
if (route == null) content()
|
||||
}
|
||||
|
||||
operator fun Route.invoke() = push(this)
|
||||
|
||||
operator fun <P0> Route.invoke(p0: P0) {
|
||||
parameters[0] = p0
|
||||
invoke()
|
||||
}
|
||||
|
||||
operator fun <P0, P1> Route.invoke(p0: P0, p1: P1) {
|
||||
parameters[1] = p1
|
||||
invoke(p0)
|
||||
}
|
||||
|
||||
operator fun <P0, P1, P2> Route.invoke(p0: P0, p1: P1, p2: P2) {
|
||||
parameters[2] = p2
|
||||
invoke(p0, p1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package it.hamy.compose.routing
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleOut
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val defaultStacking = ContentTransform(
|
||||
initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(),
|
||||
targetContentEnter = fadeIn(),
|
||||
targetContentZIndex = 1f
|
||||
)
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val defaultUnstacking = ContentTransform(
|
||||
initialContentExit = fadeOut(),
|
||||
targetContentEnter = EnterTransition.None,
|
||||
targetContentZIndex = 0f
|
||||
)
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val defaultStill = ContentTransform(
|
||||
initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(),
|
||||
targetContentEnter = fadeIn(),
|
||||
targetContentZIndex = 1f
|
||||
)
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val AnimatedContentTransitionScope<RouteHandlerScope>.isStacking: Boolean
|
||||
get() = initialState.route == null && targetState.route != null
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val AnimatedContentTransitionScope<RouteHandlerScope>.isUnstacking: Boolean
|
||||
get() = initialState.route != null && targetState.route == null
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val AnimatedContentTransitionScope<RouteHandlerScope>.isStill: Boolean
|
||||
get() = initialState.route == null && targetState.route == null
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
val AnimatedContentTransitionScope<RouteHandlerScope>.isUnknown: Boolean
|
||||
get() = initialState.route != null && targetState.route != null
|
||||
Reference in New Issue
Block a user