From 2348921c4d8abd9bf0fa807710e1ffb181eee11d Mon Sep 17 00:00:00 2001 From: CherryPerry Date: Wed, 31 Aug 2022 17:57:46 +0100 Subject: [PATCH] Refactor ChildEntry: Initialized/Suspended --- .../appyx/core/children/ChildAwareImpl.kt | 13 +--- .../bumble/appyx/core/children/ChildEntry.kt | 36 +++------ .../appyx/core/children/ChildEntryExt.kt | 4 +- .../core/children/ChildNodeCreationManager.kt | 73 +++++++++++++------ .../com/bumble/appyx/core/node/ParentNode.kt | 4 +- 5 files changed, 69 insertions(+), 61 deletions(-) diff --git a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildAwareImpl.kt b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildAwareImpl.kt index 3e323a2b8..b67f299db 100644 --- a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildAwareImpl.kt +++ b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildAwareImpl.kt @@ -5,9 +5,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.coroutineScope import com.bumble.appyx.core.lifecycle.isDestroyed +import com.bumble.appyx.core.navigation.RoutingKey import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode -import com.bumble.appyx.core.navigation.RoutingKey import com.bumble.appyx.core.withPrevious import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlin.reflect.KClass -class ChildAwareImpl : ChildAware { +class ChildAwareImpl : ChildAware { private val callbacks: MutableList = ArrayList() @@ -74,7 +74,7 @@ class ChildAwareImpl : ChildAware { val visitedSet = HashSet() commonKeys.forEach { key -> val current = value.current[key] - if (current != value.previous[key] && current is ChildEntry.Eager) { + if (current != value.previous[key] && current is ChildEntry.Initialized) { notifyWhenChanged(current.node, nodes, visitedSet) visitedSet.add(current.node) } @@ -131,11 +131,6 @@ class ChildAwareImpl : ChildAware { } private fun getCreatedNodes(childEntryMap: Map, ChildEntry<*>>) = - childEntryMap.values.mapNotNull { entry -> - when (entry) { - is ChildEntry.Eager -> entry.node - is ChildEntry.Lazy -> null - } - } + childEntryMap.values.mapNotNull { entry -> entry.nodeOrNull } } diff --git a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntry.kt b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntry.kt index cec1c7d4a..c8eb939b1 100644 --- a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntry.kt +++ b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntry.kt @@ -1,10 +1,8 @@ package com.bumble.appyx.core.children -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.node.build -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.navigation.Resolver import com.bumble.appyx.core.navigation.RoutingKey +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.state.SavedStateMap // custom equals/hashCode for MutableStateFlow and equality checks sealed class ChildEntry { @@ -20,20 +18,15 @@ sealed class ChildEntry { "$key@${javaClass.simpleName}" /** All public APIs should return this type of child which is ready to work with. */ - class Eager( + class Initialized( override val key: RoutingKey, val node: Node, ) : ChildEntry() - /** Child representation for Lazy mode. */ - class Lazy( + class Suspended( override val key: RoutingKey, - private val resolver: Resolver, - val buildContext: BuildContext, - ) : ChildEntry() { - internal fun initialize(): Eager = - Eager(key, resolver.resolve(key.routing, buildContext).build()) - } + val savedState: SavedStateMap?, + ) : ChildEntry() /** When to create child nodes? */ enum class ChildMode { @@ -46,19 +39,10 @@ sealed class ChildEntry { } - companion object { - fun create( - key: RoutingKey, - resolver: Resolver, - buildContext: BuildContext, - childMode: ChildMode, - ): ChildEntry = - when (childMode) { - ChildMode.EAGER -> - Eager(key, resolver.resolve(key.routing, buildContext).build()) - ChildMode.LAZY -> - Lazy(key, resolver, buildContext) - } + /** Keep not on screen nodes in the memory? */ + enum class KeepMode { + KEEP, + SUSPEND, } } diff --git a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntryExt.kt b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntryExt.kt index 78aeffe64..d5ee48a42 100644 --- a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntryExt.kt +++ b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntryExt.kt @@ -5,6 +5,6 @@ import com.bumble.appyx.core.node.Node val ChildEntry.nodeOrNull: Node? get() = when (this) { - is ChildEntry.Eager -> node - is ChildEntry.Lazy -> null + is ChildEntry.Initialized -> node + is ChildEntry.Suspended -> null } diff --git a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildNodeCreationManager.kt b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildNodeCreationManager.kt index 362292aa0..052acdaeb 100644 --- a/core/src/main/kotlin/com/bumble/appyx/core/children/ChildNodeCreationManager.kt +++ b/core/src/main/kotlin/com/bumble/appyx/core/children/ChildNodeCreationManager.kt @@ -5,6 +5,7 @@ import com.bumble.appyx.core.modality.AncestryInfo import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.navigation.RoutingKey import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.node.build import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.core.state.SavedStateMap import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory @@ -23,9 +24,11 @@ internal class ChildNodeCreationManager( private val _children = MutableStateFlow, ChildEntry>>(emptyMap()) val children: StateFlow> = _children.asStateFlow() + private lateinit var parentNode: ParentNode fun launch(parentNode: ParentNode) { - savedStateMap.restoreChildren(parentNode)?.also { restoredMap -> + this.parentNode = parentNode + savedStateMap.restoreChildren()?.also { restoredMap -> _children.update { restoredMap } savedStateMap = null } @@ -40,11 +43,10 @@ internal class ChildNodeCreationManager( val mutableMap = map.toMutableMap() newKeys.forEach { key -> mutableMap[key] = - ChildEntry.create( + childEntry( key = key, - resolver = parentNode, - buildContext = null.toBuildContext(parentNode), - childMode = childMode, + savedState = null, + suspended = childMode == ChildEntry.ChildMode.LAZY, ) } removedKeys.forEach { key -> @@ -56,38 +58,42 @@ internal class ChildNodeCreationManager( } } - fun childOrCreate(routingKey: RoutingKey): ChildEntry.Eager { + @Suppress("ForbiddenComment") + fun childOrCreate(routingKey: RoutingKey): ChildEntry.Initialized { + // TODO: Should not allow child creation and throw exception instead to avoid desynchronisation val value = _children.value val child = value[routingKey] ?: error( "Rendering and children management is out of sync: requested $routingKey but have only ${value.keys}" ) return when (child) { - is ChildEntry.Eager -> + is ChildEntry.Initialized -> child - is ChildEntry.Lazy -> + is ChildEntry.Suspended -> _children.updateAndGet { map -> val updateChild = map[routingKey] ?: error("Requested child $routingKey disappeared") when (updateChild) { - is ChildEntry.Eager -> map - is ChildEntry.Lazy -> map.plus(routingKey to updateChild.initialize()) + is ChildEntry.Initialized -> map + is ChildEntry.Suspended -> { + val initialized = ChildEntry.Initialized( + key = updateChild.key, + node = parentNode.resolve( + routing = updateChild.key.routing, + buildContext = childBuildContext(child.savedState), + ).build() + ) + map.plus(routingKey to initialized) + } } - }[routingKey] as ChildEntry.Eager + }[routingKey] as ChildEntry.Initialized } } - private fun SavedStateMap?.restoreChildren(parentNode: ParentNode): ChildEntryMap? = + private fun SavedStateMap?.restoreChildren(): ChildEntryMap? = (this?.get(KEY_CHILDREN_STATE) as? Map, SavedStateMap>)?.mapValues { - ChildEntry.create(it.key, parentNode, it.value.toBuildContext(parentNode), childMode) + childEntry(it.key, it.value, childMode == ChildEntry.ChildMode.LAZY) } - private fun SavedStateMap?.toBuildContext(parentNode: ParentNode): BuildContext = - BuildContext( - ancestryInfo = AncestryInfo.Child(parentNode), - savedStateMap = this, - customisations = customisations.getSubDirectoryOrSelf(parentNode::class) - ) - fun saveChildrenState(writer: MutableSavedStateMap) { val children = _children.value if (children.isNotEmpty()) { @@ -95,8 +101,8 @@ internal class ChildNodeCreationManager( children .mapValues { (_, entry) -> when (entry) { - is ChildEntry.Eager -> entry.node.saveInstanceState(writer.saverScope) - is ChildEntry.Lazy -> entry.buildContext.savedStateMap + is ChildEntry.Initialized -> entry.node.saveInstanceState(writer.saverScope) + is ChildEntry.Suspended -> entry.savedState } } if (childrenState.isNotEmpty()) { @@ -105,6 +111,29 @@ internal class ChildNodeCreationManager( } } + private fun childBuildContext(savedState: SavedStateMap?): BuildContext = + BuildContext( + ancestryInfo = AncestryInfo.Child(parentNode), + savedStateMap = savedState, + customisations = customisations.getSubDirectoryOrSelf(parentNode::class), + ) + + private fun childEntry( + key: RoutingKey, + savedState: SavedStateMap?, + suspended: Boolean, + ): ChildEntry = + if (suspended) { + ChildEntry.Suspended(key, savedState) + } else { + ChildEntry.Initialized( + key = key, + node = parentNode + .resolve(key.routing, childBuildContext(savedState)) + .build() + ) + } + private companion object { const val KEY_CHILDREN_STATE = "ChildrenState" } diff --git a/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt b/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt index 11572cc61..368ccba6a 100644 --- a/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt +++ b/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt @@ -86,7 +86,7 @@ abstract class ParentNode( manageTransitions() } - fun childOrCreate(routingKey: RoutingKey): ChildEntry.Eager = + fun childOrCreate(routingKey: RoutingKey): ChildEntry.Initialized = childNodeCreationManager.childOrCreate(routingKey) @Composable @@ -94,7 +94,7 @@ abstract class ParentNode( routing: Routing, decorator: @Composable (child: ChildRenderer) -> Unit ) { - var child by remember { mutableStateOf?>(null) } + var child by remember { mutableStateOf?>(null) } LaunchedEffect(routing) { permanentNavModel.elements.collect { elements -> val routingKey = elements.find { it.key.routing == routing }?.key