This commit is contained in:
2024-02-27 22:09:30 +03:00
parent bfa3231823
commit 38a3141d43
479 changed files with 36348 additions and 10142 deletions

1
compose/routing/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

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

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

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

View File

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

View File

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

View File

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

View File

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