diff --git a/nodal/src/androidMain/kotlin/dev.omkartenkale.nodal/NodalActivity.kt b/nodal/src/androidMain/kotlin/dev.omkartenkale.nodal/NodalActivity.kt index 5fbe72c..25d3e2e 100644 --- a/nodal/src/androidMain/kotlin/dev.omkartenkale.nodal/NodalActivity.kt +++ b/nodal/src/androidMain/kotlin/dev.omkartenkale.nodal/NodalActivity.kt @@ -65,14 +65,13 @@ public abstract class NodalActivity : AppCompatActivity() { provides { UI().also { ui = it - container.addView(ComposeView(this@NodalActivity).also { setContent { ui.drawLayers() } }) + container.addView(ComposeView(this@NodalActivity).also { setContent { ui.Content() } }) } } include(dependencyDeclaration) }.also { RootNodeUtil.dispatchAdded(it) } - container.addView(ComposeView(this).also { setContent { ui.drawLayers() } }) } @CallSuper diff --git a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt index 1ad0425..acf97ff 100644 --- a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt +++ b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt @@ -2,31 +2,30 @@ package dev.omkartenkale.nodal.compose import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import dev.omkartenkale.nodal.Node import dev.omkartenkale.nodal.Node.Companion.ui +import dev.omkartenkale.nodal.compose.transitions.Backstack +import dev.omkartenkale.nodal.compose.transitions.BackstackTransition import dev.omkartenkale.nodal.util.doOnRemoved import kotlinx.coroutines.flow.MutableStateFlow public class UI { - private val layers = mutableStateListOf() - + private var layers by mutableStateOf>(emptyList()) public val focusState: MutableStateFlow = MutableStateFlow(false) - @Composable - public fun drawLayers() { - layers.forEach { - it.draw() - } + public fun Content() { + Backstack(backstack = layers) } public fun draw(content: @Composable (Modifier) -> Unit): Layer { - return Layer(content) { - layers.remove(it) + return Layer(content = content) { + layers -= it }.also { - layers.add(it) + layers += it } } @@ -34,19 +33,22 @@ public class UI { focusState.emit(isFocused) } - public class Layer(public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) { + public class Layer(public val transition: BackstackTransition = BackstackTransition.None, public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) { @Composable - public fun draw() { + public fun Content() { content(Modifier.fillMaxSize()) } public fun destroy() { onDestroy(this) } + } } +private fun List.secondToTop(): UI.Layer? = if(size < 2 ) null else get(lastIndex-1) + public fun Node.draw(content: @Composable (Modifier) -> Unit) { val layer = ui.draw(content) doOnRemoved { diff --git a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/Backstack.kt b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/Backstack.kt new file mode 100644 index 0000000..4948863 --- /dev/null +++ b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/Backstack.kt @@ -0,0 +1,148 @@ +package dev.omkartenkale.nodal.compose.transitions + + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import dev.omkartenkale.nodal.compose.UI + +/** + * Identifies which direction a transition is being performed in. + */ +internal enum class TransitionDirection { + Forward, + Backward +} + +/** + * Fork of https://github.com/rjrjr/compose-backstack + * + * Renders the top of a stack of screens (as [T]s) and animates between screens when the top + * value changes. Any state used by a screen will be preserved as long as it remains in the stack + * (i.e. result of [remember] calls). + * + * The [backstack] must follow some rules: + * - Must always contain at least one item. + * - Items in the stack must implement `equals` and not change over the lifetime of the screen. + * If an item changes, it will be considered a new screen and any state held by the screen will + * be lost. + * - If items in the stack are reordered between compositions, the stack should not contain + * duplicates. If it does, due to how `@Pivotal` works, the states of those screens will be + * lost if they are moved around. If the list contains duplicates, an [IllegalArgumentException] + * will be thrown. + * + * This composable does not actually provide any navigation functionality – it just manages state, + * and delegates to [FrameController]s to do things like animate screen transitions. It can be + * plugged into your navigation library of choice, or just used on its own with a simple list of + * screens. + * + * ## Saveable state caching + * + * Screens that contain persistable state using [rememberSaveable] will automatically have that + * state saved when they are hidden, and restored the next time they're shown. + * + * ## Example + * + * ``` + * sealed class Screen { + * object ContactList: Screen() + * data class ContactDetails(val id: String): Screen() + * data class EditContact(val id: String): Screen() + * } + * + * data class Navigator( + * val push: (Screen) -> Unit, + * val pop: () -> Unit + * ) + * + * @Composable fun App() { + * var backstack: List by remember { mutableStateOf(listOf(Screen.ContactList)) } + * val navigator = remember { + * Navigator( + * push = { backstack += it }, + * pop = { backstack = backstack.dropLast(1) } + * ) + * } + * + * Backstack(backstack) { screen -> + * when(screen) { + * Screen.ContactList -> ShowContactList(navigator) + * is Screen.ContactDetails -> ShowContact(screen.id, navigator) + * is Screen.EditContact -> ShowEditContact(screen.id, navigator) + * } + * } + * } + * ``` + * + * @param backstack The stack of screen values. + * @param modifier [Modifier] that will be applied to the container of screens. Neither affects nor + * is affected by transition animations. + * @param frameController The [FrameController] that manages things like transition animations. + * Use [rememberTransitionController] for a reasonable default, or use the overload of this function + * that takes a [BackstackTransition] instead. + * @param content Called with each element of [backstack] to render it. + */ +@Composable +internal fun Backstack( + backstack: List, + modifier: Modifier = Modifier, + frameController: TransitionController, +) { + + // Notify the frame controller that the backstack has changed to allow it to do stuff like start + // animating transitions. This call should eventually cause activeFrames to change, but that might + // not happen immediately. + // + // Note: It's probably bad that this call is not done in a side effect. If the composition fails, + // the controller won't know about it and will continue animating or whatever it was doing. + // However, we do need to give the controller the chance to initialize itself with the initial + // stack before we ask for its activeFrames, so this is a lazy way to do both that and subsequent + // updates. + frameController.updateBackstack(backstack) + + // Actually draw the screens. + Box(modifier = modifier.clip(RectangleShape)) { + // The frame controller is in complete control of what we actually show. The activeFrames + // property should be backed by a snapshot state object, so this will recompose automatically + // if the controller changes its frames. + frameController.activeFrames.forEach { (item, frameControlModifier) -> + // Even if screens are moved around within the list, as long as they're invoked through the + // exact same sequence of source locations from within this key lambda, they will keep their + // state. + key(item) { + // This call must be inside the key(){} wrapper. + Box(frameControlModifier) { + item.Content() + } + } + } + } +} + +/** + * Renders the top of a stack of screens (as [T]s) and animates between screens when the top + * value changes. Any state used by a screen will be preserved as long as it remains in the stack + * (i.e. result of [remember] calls). + * + * See the documentation on [Backstack] for more information. + * + * @param backstack The stack of screen values. + * @param modifier [Modifier] that will be applied to the container of screens. Neither affects nor + * is affected by transition animations. + * @param transition The [BackstackTransition] to use to animate screen transitions. For more, + * call [rememberTransitionController] and pass it to the overload of this function that takes a + * [FrameController] directly. + * @param content Called with each element of [backstack] to render it. + */ +@Composable internal fun Backstack( + backstack: List, + modifier: Modifier = Modifier, + transition: BackstackTransition = BackstackTransition.Slide +) { + Backstack(backstack, modifier, rememberTransitionController(transition)) +} \ No newline at end of file diff --git a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/BackstackTransition.kt b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/BackstackTransition.kt new file mode 100644 index 0000000..ff63f7d --- /dev/null +++ b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/BackstackTransition.kt @@ -0,0 +1,102 @@ +package dev.omkartenkale.nodal.compose.transitions + +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize + +/** + * Defines transitions for a [Backstack]. Transitions control how screens are rendered by returning + * [Modifier]s that will be used to wrap screen composables. + * + * @see Slide + * @see Crossfade + */ +public fun interface BackstackTransition { + + /** + * Returns a [Modifier] to use to draw screen in a [Backstack]. + * + * @param visibility A float in the range `[0, 1]` that indicates at what visibility this screen + * should be drawn. For example, this value will increase when [isTop] is true and the transition + * is in the forward direction. + * @param isTop True only when being called for the top screen. E.g. if the screen is partially + * visible, then the top screen is always transitioning _out_, and non-top screens are either + * transitioning out or invisible. + */ + public fun Modifier.modifierForScreen( + visibility: State, + isTop: Boolean + ): Modifier + + /** + * A simple transition that slides screens horizontally. + */ + public object Slide : BackstackTransition { + override fun Modifier.modifierForScreen( + visibility: State, + isTop: Boolean + ): Modifier = then(PercentageLayoutOffset( + rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 + visibility.value } + )) + + + internal class PercentageLayoutOffset(private val rawOffset: State) : + LayoutModifier { + private val offset = { rawOffset.value.coerceIn(-1f..1f) } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + val placeable = measurable.measure(constraints) + return layout(placeable.width, placeable.height) { + placeable.place(offsetPosition(IntSize(placeable.width, placeable.height))) + } + } + + internal fun offsetPosition(containerSize: IntSize) = IntOffset( + // RTL is handled automatically by place. + x = (containerSize.width * offset()).toInt(), + y = 0 + ) + + override fun toString(): String = "${this::class.simpleName}(offset=$offset)" + } + } + + /** + * A simple transition that crossfades between screens. + */ + public object Crossfade : BackstackTransition { + override fun Modifier.modifierForScreen( + visibility: State, + isTop: Boolean + ): Modifier = alpha(visibility.value) + } + + /** + * A simple transition that crossfades between screens. + */ + public object None : BackstackTransition { + override fun Modifier.modifierForScreen( + visibility: State, + isTop: Boolean + ): Modifier = this + } +} + +/** + * Convenience function to make it easier to make composition transitions. + */ +public fun BackstackTransition.modifierForScreen( + visibility: State, + isTop: Boolean +): Modifier = Modifier.modifierForScreen(visibility, isTop) \ No newline at end of file diff --git a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/FrameController.kt b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/FrameController.kt new file mode 100644 index 0000000..1b304b5 --- /dev/null +++ b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/FrameController.kt @@ -0,0 +1,55 @@ +package dev.omkartenkale.nodal.compose.transitions + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier + +/** + * A stable object that processes changes to a [Backstack]'s list of screen keys, determining which + * screens should be actively composed at any given time, and tweaking their appearance by applying + * [Modifier]s. + * + * The [Backstack] composable will notify its controller whenever the backstack changes by calling + * [updateBackstack], but the controller is in full control of when those changes actually get + * reflected in the composition. For example, a controller may choose to keep some screens around + * for a while, even after they're removed from the backstack, in order to animate their removal. + */ +@Stable +internal interface FrameController { + + /** + * The frames that are currently being active. All active frames will be composed. When a frame + * that is in the backstack stops appearing in this list, its state will be saved. + * + * Should be backed by either a [MutableState] or a [SnapshotStateList]. This property + * will not be read until after [updateBackstack] is called at least once. + */ + val activeFrames: List> + + /** + * Notifies the controller that a new backstack was passed in. This method must initialize + * [activeFrames] first time it's called, and subsequently should probably result in + * [activeFrames] being updated to show new keys or hide old ones, although the controller may + * choose to do that later (e.g. if one of the active frames is currently being animated). + * + * This method will be called _directly from the composition_ – it must not perform side effects + * or update any state that is not backed by snapshot state objects (such as [MutableState]s, + * lists created by [mutableStateListOf], etc.). + * + * @param keys The latest backstack passed to [Backstack]. Will always contain at least one + * element. + */ + fun updateBackstack(keys: List) + + /** + * A frame controlled by a [FrameController], to be shown by [Backstack]. + */ + @Immutable + data class BackstackFrame( + val key: T, + val modifier: Modifier = Modifier + ) +} \ No newline at end of file diff --git a/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/TransitionController.kt b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/TransitionController.kt new file mode 100644 index 0000000..c51d3a4 --- /dev/null +++ b/nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/TransitionController.kt @@ -0,0 +1,191 @@ +package dev.omkartenkale.nodal.compose.transitions + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.TweenSpec +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import dev.omkartenkale.nodal.compose.UI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * Returns the default [AnimationSpec] used for [rememberTransitionController]. + */ +@Composable internal fun defaultBackstackAnimation(): AnimationSpec { + return TweenSpec(durationMillis = 200) +} + +/** + * Returns a [FrameController] that will animate transitions between screens. + * + * @param transition The [BackstackTransition] that defines how to animate between screens when + * the backstack changes. [BackstackTransition] contains a few simple pre-fab transitions. + * @param animationSpec Defines the curve and speed of transition animations. + * @param onTransitionStarting Callback that will be invoked before starting each transition. + * @param onTransitionFinished Callback that will be invoked after each transition finishes. + */ +@Composable internal fun rememberTransitionController( + transition: BackstackTransition = BackstackTransition.Slide, + animationSpec: AnimationSpec = defaultBackstackAnimation(), + onTransitionStarting: (from: List, to: List, TransitionDirection) -> Unit = { _, _, _ -> }, + onTransitionFinished: () -> Unit = {}, +): TransitionController { + val scope = rememberCoroutineScope() + return remember { TransitionController(scope) }.also { + it.transition = transition + it.animationSpec = animationSpec + it.onTransitionStarting = onTransitionStarting + it.onTransitionFinished = onTransitionFinished + + LaunchedEffect(it) { + it.runTransitionAnimations() + } + } +} + +/** + * A [FrameController] that implements transition modifiers specified by [BackstackTransition]s. + * + * @param scope The [CoroutineScope] used for animations. + */ +internal class TransitionController( + private val scope: CoroutineScope +) : FrameController { + + /** + * Holds information about an in-progress transition. + */ + @Immutable + private data class ActiveTransition( + val fromFrame: FrameController.BackstackFrame, + val toFrame: FrameController.BackstackFrame, + val popping: Boolean + ) + + internal var transition: BackstackTransition? by mutableStateOf(null) + internal var animationSpec: AnimationSpec? by mutableStateOf(null) + internal var onTransitionStarting: ((from: List, to: List, TransitionDirection) -> Unit)? + by mutableStateOf(null) + internal var onTransitionFinished: (() -> Unit)? by mutableStateOf(null) + + /** + * A snapshot of the backstack that will remain unchanged during transitions, even if + * [updateBackstack] is called with a different stack. Just before + * [starting a transition][animateTransition], this list will be used to determine if we should use + * a forwards or backwards animation. It's a [MutableState] because it is used to derive the value + * for [activeFrames], and so it needs to be observable. + */ + private var displayedKeys: List by mutableStateOf(emptyList()) + + /** The latest list of keys seen by [updateBackstack]. */ + private var targetKeys by mutableStateOf(emptyList()) + + /** + * Set to a non-null value only when actively animating between screens as the result of a call + * to [updateBackstack]. This is a [MutableState] because it's used to derive the value of + * [activeFrames], and so it needs to be observable. + */ + private var activeTransition: ActiveTransition? by mutableStateOf(null) + + override val activeFrames: List> by derivedStateOf { + activeTransition?.let { transition -> + if (transition.popping) { + displayedKeys.dropLast(1).map { FrameController.BackstackFrame(it) } + listOf(transition.toFrame, transition.fromFrame) + } else { + displayedKeys.dropLast(2).map { FrameController.BackstackFrame(it) } + listOf( + transition.fromFrame, + transition.toFrame + ) + } + } ?: displayedKeys.map { FrameController.BackstackFrame(it) } + } + + /** + * Should be called from a coroutine that has access to a frame clock (i.e. from a + * [rememberCoroutineScope] or in a [LaunchedEffect]), and must be allowed to run until this + * [TransitionController] leaves the composition. It will never return unless cancelled. + */ + suspend fun runTransitionAnimations() { + // This flow handles backpressure by conflating: if targetKeys is changed multiple times while + // an animation is running, we'll only get a single emission when it finishes. + snapshotFlow { targetKeys }.collect { targetKeys -> + if (displayedKeys.last() == targetKeys.last()) { + // The visible screen didn't change, so we don't need to animate, but we need to update our + // active list for the next time we check for navigation direction. + displayedKeys = targetKeys + return@collect + } + + // The top of the stack was changed, so animate to the new top. + animateTransition(fromKeys = displayedKeys, toKeys = targetKeys) + } + } + + override fun updateBackstack(keys: List) { + // Always remember the latest stack, so if this call is happening during a transition we can + // detect that when the transition finishes and start the next transition. + targetKeys = keys + + // This is the first update, so we don't animate, and need to show the backstack as-is + // immediately. + if (displayedKeys.isEmpty()) { + displayedKeys = keys + } + } + + /** + * Called when [updateBackstack] gets a new backstack with a new top frame while idle, or after a + * transition if the [targetKeys]' top is not [displayedKeys]' top. + */ + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun animateTransition(fromKeys: List, toKeys: List) { + check(activeTransition == null) { "Can only start transitioning while idle." } + + val fromKey = fromKeys.last() + val toKey = toKeys.last() + val popping = toKey in fromKeys + val progress = Animatable(0f) + + val fromVisibility = derivedStateOf { 1f - progress.value } + val toVisibility = progress.asState() + + // Wrap modifier functions in each their own recompose scope so that if they read the visibility + // (or any other state) directly, the modified node will actually be updated. + val fromModifier = Modifier.composed { + with(fromKey.transition) { + modifierForScreen(fromVisibility, isTop = popping) + } + } + val toModifier = Modifier.composed { + with(toKey.transition) { + modifierForScreen(toVisibility, isTop = !popping) + } + } + + activeTransition = ActiveTransition( + fromFrame = FrameController.BackstackFrame(fromKey, fromModifier), + toFrame = FrameController.BackstackFrame(toKey, toModifier), + popping = popping + ) + + val oldActiveKeys = displayedKeys + displayedKeys = targetKeys + + onTransitionStarting!!(oldActiveKeys, displayedKeys, if (popping) TransitionDirection.Backward else TransitionDirection.Forward) + progress.animateTo(1f, animationSpec!!) + activeTransition = null + onTransitionFinished!!() + } +} \ No newline at end of file diff --git a/samples/ride/src/commonMain/composeResources/drawable/home.png b/samples/ride/src/commonMain/composeResources/drawable/home.png new file mode 100644 index 0000000..d2c7170 Binary files /dev/null and b/samples/ride/src/commonMain/composeResources/drawable/home.png differ diff --git a/samples/ride/src/commonMain/composeResources/drawable/home_tabs.png b/samples/ride/src/commonMain/composeResources/drawable/home_tabs.png new file mode 100644 index 0000000..ed025a1 Binary files /dev/null and b/samples/ride/src/commonMain/composeResources/drawable/home_tabs.png differ diff --git a/samples/ride/src/commonMain/composeResources/drawable/myaccount.png b/samples/ride/src/commonMain/composeResources/drawable/myaccount.png new file mode 100644 index 0000000..306e8bf Binary files /dev/null and b/samples/ride/src/commonMain/composeResources/drawable/myaccount.png differ diff --git a/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/LoggedInNode.kt b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/LoggedInNode.kt index 22f120d..2d89532 100644 --- a/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/LoggedInNode.kt +++ b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/LoggedInNode.kt @@ -1,10 +1,24 @@ package dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import dev.omkartenkale.nodal.Node +import dev.omkartenkale.nodal.compose.UI +import dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin.myaccoubt.MyAccountNode import dev.omkartenkale.nodal.util.addChild import dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin.ride.RideNode import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import nodal.ride.generated.resources.Res +import nodal.ride.generated.resources.home +import nodal.ride.generated.resources.home_tabs +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource fun Node.addLoggedInNode(userName: String) = addChild(LoggedInNode.Args(userName)) @@ -12,12 +26,49 @@ class LoggedInNode: Node() { class Args(val userName: String) private val args: Args by dependencies() + private lateinit var layer: UI.Layer + + @OptIn(ExperimentalResourceApi::class) override fun onAdded() { - childrenUpdatedEvents.onEach { - if(children.isEmpty()){ - removeSelf() + + layer = ui.draw { + + println("LoggedInNode: Recompose") + LaunchedEffect(Unit){ + println("LoggedInNode: LaunchedEffect") + } + + Column { + Image( + modifier = Modifier.fillMaxWidth().weight(1f).clickable { + addChild() + }, + painter = painterResource(Res.drawable.home), + contentScale = ContentScale.FillWidth, + contentDescription = null + ) + + Image( + modifier = Modifier.fillMaxWidth().clickable { + addChild() + }, + painter = painterResource(Res.drawable.home_tabs), + contentScale = ContentScale.FillWidth, + contentDescription = null + ) } - }.launchIn(coroutineScope) - addChild() + } + +// childrenUpdatedEvents.onEach { +// if(children.isEmpty()){ +// removeSelf() +// } +// }.launchIn(coroutineScope) + + } + + override fun onRemoved() { + super.onRemoved() + layer.destroy() } } diff --git a/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/myaccoubt/MyAccountNode.kt b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/myaccoubt/MyAccountNode.kt new file mode 100644 index 0000000..0d9535b --- /dev/null +++ b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/myaccoubt/MyAccountNode.kt @@ -0,0 +1,49 @@ +package dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin.myaccoubt + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import dev.omkartenkale.nodal.Node +import dev.omkartenkale.nodal.compose.UI +import dev.omkartenkale.nodal.util.addChild +import dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin.ride.RideNode +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import nodal.ride.generated.resources.Res +import nodal.ride.generated.resources.home +import nodal.ride.generated.resources.myaccount +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +class MyAccountNode: Node() { + + private lateinit var layer: UI.Layer + + @OptIn(ExperimentalResourceApi::class) + override fun onAdded() { + + layer = ui.draw { + Column { + + Image( + modifier = Modifier.fillMaxWidth().wrapContentHeight().clickable { +// addChild() + removeSelf() + }, + painter = painterResource(Res.drawable.myaccount), + contentScale = ContentScale.FillWidth, + contentDescription = null + ) + } + } + } + + override fun onRemoved() { + super.onRemoved() + layer.destroy() + } +} diff --git a/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/ride/RideNode.kt b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/ride/RideNode.kt index 13953ff..72a81ca 100644 --- a/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/ride/RideNode.kt +++ b/samples/ride/src/commonMain/kotlin/dev.omkartenkale.nodal.sample.ride/nodes/root/loggedin/ride/RideNode.kt @@ -4,6 +4,8 @@ package dev.omkartenkale.nodal.sample.ride.nodes.root.loggedin.ride import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import dev.omkartenkale.nodal.Node @@ -26,11 +28,11 @@ class RideNode: Node() { private lateinit var layer: UI.Layer override fun onAdded() { - childrenUpdatedEvents.onEach { - if(children.isEmpty()){ - removeSelf() - } - }.launchIn(coroutineScope) +// childrenUpdatedEvents.onEach { +// if(children.isEmpty()){ +// removeSelf() +// } +// }.launchIn(coroutineScope) layer = ui.draw { Image( modifier = it.clickable {