From e9b8a0013e560bea848a3fb31b931f19e71731bf Mon Sep 17 00:00:00 2001 From: Isaac Udy Date: Tue, 7 May 2024 21:06:58 +1200 Subject: [PATCH] Added `EmbeddedNavigationDestination` as an experimental API, which allows a `NavigationKey.SupportsPush` to be rendered as an embedded destination within another Composable. --- CHANGELOG.md | 1 + .../compose/EmbeddedNavigationDestination.kt | 69 +++++++++++ .../compose/EmbeddedDestination.kt | 110 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 enro-core/src/main/java/dev/enro/destination/compose/EmbeddedNavigationDestination.kt create mode 100644 tests/application/src/main/java/dev/enro/tests/application/compose/EmbeddedDestination.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 413a2234..bc2f1ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * EnroBackConfiguration.Manual disables all back handling via Enro, and allows developers to set their own back pressed handling for individual destinations * EnroBackConfiguration.Predictive is experimental, but adds support for predictive back gestures and animations. This is not yet fully implemented, and is not recommended for production use. Once this is stabilised, EnroBackNavigation.Default will be renamed to EnroBackNavigation.Legacy, and EnroBackNavigation.Predictive will become the default. * Removed `ContainerRegistrationStrategy` from the "core" `rememberNavigationContainer` methods, to stop the requirement to opt-in for `AdvancedEnroApi` when using the standard `rememberNavigationContainer` APIs. This was introduced accidentally with 2.4.0. +* Added `EmbeddedNavigationDestination` as an experimental API, which allows a `NavigationKey.SupportsPush` to be rendered as an embedded destination within another Composable. ## 2.4.0 * Updated dependency versions diff --git a/enro-core/src/main/java/dev/enro/destination/compose/EmbeddedNavigationDestination.kt b/enro-core/src/main/java/dev/enro/destination/compose/EmbeddedNavigationDestination.kt new file mode 100644 index 00000000..7c221392 --- /dev/null +++ b/enro-core/src/main/java/dev/enro/destination/compose/EmbeddedNavigationDestination.kt @@ -0,0 +1,69 @@ +package dev.enro.destination.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import dev.enro.annotations.ExperimentalEnroApi +import dev.enro.core.NavigationKey +import dev.enro.core.compose.rememberNavigationContainer +import dev.enro.core.container.EmptyBehavior +import dev.enro.core.container.acceptNone + +@Composable +@ExperimentalEnroApi +public fun EmbeddedNavigationDestination( + navigationKey: NavigationKey.SupportsPush, + onClosed: (() -> Unit), + modifier: Modifier = Modifier, +) { + val rememberedOnClosed = rememberUpdatedState(onClosed) + val container = rememberNavigationContainer( + root = navigationKey, + emptyBehavior = EmptyBehavior.CloseParent, + filter = acceptNone(), + interceptor = { + onClosed { + if (it != navigationKey) return@onClosed continueWithClose() + rememberedOnClosed.value.invoke() + cancelClose() + } + } + ) + Box(modifier = modifier) { + container.Render() + } +} + +@Composable +@ExperimentalEnroApi +public inline fun EmbeddedNavigationDestination( + navigationKey: NavigationKey.SupportsPush.WithResult, + noinline onClosed: (() -> Unit), + modifier: Modifier = Modifier, + noinline onResult: (T) -> Unit = {}, +) { + val rememberedOnClosed = rememberUpdatedState(onClosed) + val rememberedOnResult = rememberUpdatedState(onResult) + + val container = rememberNavigationContainer( + emptyBehavior = EmptyBehavior.CloseParent, + root = navigationKey, + filter = acceptNone(), + interceptor = { + onClosed { + if (it != navigationKey) return@onClosed continueWithClose() + rememberedOnClosed.value.invoke() + cancelClose() + } + onResult, T> { key, result -> + if (key != navigationKey) return@onResult continueWithClose() + rememberedOnResult.value.invoke(result) + cancelResult() + } + } + ) + Box(modifier = modifier) { + container.Render() + } +} \ No newline at end of file diff --git a/tests/application/src/main/java/dev/enro/tests/application/compose/EmbeddedDestination.kt b/tests/application/src/main/java/dev/enro/tests/application/compose/EmbeddedDestination.kt new file mode 100644 index 00000000..bb4d25eb --- /dev/null +++ b/tests/application/src/main/java/dev/enro/tests/application/compose/EmbeddedDestination.kt @@ -0,0 +1,110 @@ +package dev.enro.tests.application.compose + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import dev.enro.annotations.ExperimentalEnroApi +import dev.enro.annotations.NavigationDestination +import dev.enro.core.NavigationKey +import dev.enro.core.close +import dev.enro.core.closeWithResult +import dev.enro.core.compose.navigationHandle +import dev.enro.destination.compose.EmbeddedNavigationDestination +import dev.enro.tests.application.compose.common.TitledColumn +import kotlinx.parcelize.Parcelize + +@Parcelize +object EmbeddedDestination : NavigationKey.SupportsPush { + @Parcelize + internal data object NoResult: NavigationKey.SupportsPush + + @Parcelize + internal data object WithResult: NavigationKey.SupportsPush.WithResult +} + +@OptIn(ExperimentalEnroApi::class) +@Composable +@NavigationDestination(EmbeddedDestination::class) +fun EmbeddedDestination() { + val navigation = navigationHandle() + var withResult by remember { + mutableStateOf(false) + } + + var lastResult by remember { + mutableStateOf("(None)") + } + + TitledColumn(title = "Embedded Destination") { + + Text(text = "Last result: $lastResult") + + Button(onClick = { withResult = !withResult }) { + Text(text = "Toggle Result") + } + + Crossfade( + modifier = Modifier.weight(1f), + targetState = withResult + ) { withResult -> + if (withResult) { + EmbeddedNavigationDestination( + navigationKey = EmbeddedDestination.WithResult, + modifier = Modifier.fillMaxSize(), + onClosed = { + navigation.close() + }, + onResult = { + lastResult = it + } + ) + } + else { + EmbeddedNavigationDestination( + navigationKey = EmbeddedDestination.NoResult, + onClosed = { + navigation.close() + }, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} + +@Composable +@NavigationDestination(EmbeddedDestination.NoResult::class) +fun EmbeddedDestinationNoResult() { + val navigation = navigationHandle() + TitledColumn(title = "No Result Destination") { + Text(text = "This destination has no result") + Button(onClick = { navigation.close() }) { + Text(text = "Close") + } + } +} + +@Composable +@NavigationDestination(EmbeddedDestination.WithResult::class) +fun EmbeddedDestinationWithResult() { + val navigation = navigationHandle() + TitledColumn(title = "No Result Destination") { + Text(text = "This destination can return results") + Button(onClick = { navigation.closeWithResult("A") }) { + Text(text = "Send Result (A)") + } + Button(onClick = { navigation.closeWithResult("B") }) { + Text(text = "Send Result (B)") + } + Button(onClick = { navigation.close() }) { + Text(text = "Close") + } + } +} \ No newline at end of file