Skip to content

Instantly share code, notes, and snippets.

@Skeptick
Created February 21, 2025 08:27
Show Gist options
  • Save Skeptick/7c354c80b9941576e85c3c3367c57f76 to your computer and use it in GitHub Desktop.
Save Skeptick/7c354c80b9941576e85c3c3367c57f76 to your computer and use it in GitHub Desktop.
Decompose-Router Result API
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import com.arkivanov.decompose.Cancellation
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.essenty.lifecycle.Lifecycle
import io.github.xxfast.decompose.router.LocalRouterContext
import io.github.xxfast.decompose.router.slot.Router as SlotRouter
import io.github.xxfast.decompose.router.stack.Router as StackRouter
@Composable
fun <C : Any> NavigationHandler(
router: StackRouter<C>,
onPop: () -> Unit
) {
val context = LocalRouterContext.current
val currentOnPop by rememberUpdatedState(onPop)
var previousConfig by remember { mutableStateOf<C?>(null) }
var navigationSubscription by remember { mutableStateOf<Cancellation?>(null) }
val navigationCallback = remember {
{ _: StackNavigation.Event<C> ->
if (router.stack.value.active.configuration == previousConfig) {
currentOnPop()
}
}
}
val lifecycleCallback = remember {
object : Lifecycle.Callbacks {
override fun onResume() {
previousConfig = router.stack.value.backStack.lastOrNull()?.configuration
navigationSubscription = router.subscribe(navigationCallback)
}
}
}
DisposableEffect(Unit) {
context.lifecycle.subscribe(lifecycleCallback)
onDispose {
navigationSubscription?.cancel()
context.lifecycle.unsubscribe(lifecycleCallback)
}
}
}
@Composable
fun <C : Any> NavigationHandler(
router: SlotRouter<C>,
onDismiss: () -> Unit
) {
val currentOnDismiss by rememberUpdatedState(onDismiss)
val navigationCallback = remember {
{ _: SlotNavigation.Event<C> ->
if (router.slot.value.child == null) {
currentOnDismiss()
}
}
}
DisposableEffect(Unit) {
val subscription = router.subscribe(navigationCallback)
onDispose(subscription::cancel)
}
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
val LocalResultManager = staticCompositionLocalOf<ResultManager> {
RouterResultManager
}
@Composable
fun rememberResultManager(): ResultManager {
return remember { RouterResultManager }
}
interface ResultManager {
fun register(key: String, result: Any)
fun consume(key: String): Any?
fun isRegistered(key: String): Boolean
}
@Stable
internal object RouterResultManager : ResultManager {
private val results = mutableStateMapOf<String, Any>()
override fun register(key: String, result: Any) {
results[key] = result
}
override fun consume(key: String): Any? {
return results.remove(key)
}
override fun isRegistered(key: String): Boolean {
return results.containsKey(key)
}
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.remember
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import io.github.xxfast.decompose.router.slot.Router as SlotRouter
import io.github.xxfast.decompose.router.stack.Router as StackRouter
inline fun <R : Route, C : R> StackRouter<R>.pushForResult(
key: RouteAwareKey<C>,
configuration: C,
crossinline onComplete: () -> Unit = {}
) {
pushNew(configuration) {
stack.value.active.instance.setResultKey(configuration, key, canReplace = false)
onComplete()
}
}
inline fun <R : Route, C : R> SlotRouter<R>.activateForResult(
key: RouteAwareKey<C>,
configuration: C,
crossinline onComplete: () -> Unit = {}
) {
activate(configuration) {
slot.value.child?.instance?.setResultKey(configuration, key, canReplace = true)
onComplete()
}
}
inline fun <R : Route> StackRouter<R>.popWithResult(
resultManager: ResultManager,
result: Any,
crossinline onComplete: (isSuccess: Boolean) -> Unit = {}
) {
getResultKey()?.let { resultManager.register(it.value, result) }
pop(onComplete)
}
inline fun <R : Route> SlotRouter<R>.dismissWithResult(
resultManager: ResultManager,
result: Any,
crossinline onComplete: (isSuccess: Boolean) -> Unit = {}
) {
getResultKey()?.let { resultManager.register(it.value, result) }
dismiss(onComplete)
}
@Composable
@NonRestartableComposable
inline fun <R : Route> SlotRouter<R>.setResultOnDismiss(crossinline resultBuilder: () -> Any) {
val resultManager = LocalResultManager.current
val resultKey = remember { getResultKey()?.value } ?: return
NavigationHandler(router = this) {
if (!resultManager.isRegistered(resultKey)) {
resultManager.register(resultKey, resultBuilder())
}
}
}
@Composable
@NonRestartableComposable
inline fun <R : Route> StackRouter<R>.setResultOnPop(crossinline resultBuilder: () -> Any) {
val resultManager = LocalResultManager.current
val resultKey = remember { getResultKey()?.value } ?: return
NavigationHandler(router = this) {
if (!resultManager.isRegistered(resultKey)) {
resultManager.register(resultKey, resultBuilder())
}
}
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import kotlinx.serialization.Serializable
import io.github.xxfast.decompose.router.RouterContext
import io.github.xxfast.decompose.router.slot.Router as SlotRouter
import io.github.xxfast.decompose.router.stack.Router as StackRouter
import kotlin.jvm.JvmInline
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
/**
* Marker interface for all routes
*/
@Serializable
@Immutable
sealed interface Route
/**
* Marker interface for routes that can return value
*/
interface Returnable<T : Any>
@JvmInline
@Immutable
value class RouteAwareKey<T : Route>(val value: String) : InstanceKeeper.Instance
@OptIn(ExperimentalUuidApi::class)
@Suppress("UNCHECKED_CAST")
@Composable
inline fun <R : Any, C> ResultManager.rememberResultKey(
crossinline onResult: @DisallowComposableCalls (R) -> Unit
): RouteAwareKey<C> where C : Route, C : Returnable<R> {
val key = rememberSaveable { Uuid.random().toString() }
val hasResult = isRegistered(key)
LaunchedEffect(hasResult) {
if (hasResult) onResult(consume(key) as R)
}
return remember(key) { RouteAwareKey(key) }
}
@PublishedApi
internal fun <R : Route> StackRouter<R>.getResultKey(): RouteAwareKey<*>? {
val active = stack.value.active
return active.instance.getResultKey(active.configuration)
}
@PublishedApi
internal fun <R : Route> SlotRouter<R>.getResultKey(): RouteAwareKey<*>? {
val child = slot.value.child
return child?.instance?.getResultKey(child.configuration)
}
@PublishedApi
internal fun RouterContext.getResultKey(route: Route): RouteAwareKey<*>? {
return instanceKeeper.get(route.resultKey) as? RouteAwareKey<*>
}
@PublishedApi
internal fun RouterContext.setResultKey(route: Route, key: RouteAwareKey<*>, canReplace: Boolean = false) {
val instanceKey = route.resultKey
if (canReplace) instanceKeeper.remove(instanceKey)
instanceKeeper.put(instanceKey, instance = key)
}
private val Route.resultKey: String get() = "$this.result_key"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment