Skip to content

Commit

Permalink
Refactor ChildEntry: Initialized/Suspended
Browse files Browse the repository at this point in the history
  • Loading branch information
CherryPerry committed Sep 6, 2022
1 parent a78bbff commit 2348921
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ 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
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlin.reflect.KClass

class ChildAwareImpl<N: Node> : ChildAware<N> {
class ChildAwareImpl<N : Node> : ChildAware<N> {

private val callbacks: MutableList<ChildAwareCallbackInfo> = ArrayList()

Expand Down Expand Up @@ -74,7 +74,7 @@ class ChildAwareImpl<N: Node> : ChildAware<N> {
val visitedSet = HashSet<Node>()
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)
}
Expand Down Expand Up @@ -131,11 +131,6 @@ class ChildAwareImpl<N: Node> : ChildAware<N> {
}

private fun getCreatedNodes(childEntryMap: Map<out RoutingKey<*>, ChildEntry<*>>) =
childEntryMap.values.mapNotNull { entry ->
when (entry) {
is ChildEntry.Eager -> entry.node
is ChildEntry.Lazy -> null
}
}
childEntryMap.values.mapNotNull { entry -> entry.nodeOrNull }

}
36 changes: 10 additions & 26 deletions core/src/main/kotlin/com/bumble/appyx/core/children/ChildEntry.kt
Original file line number Diff line number Diff line change
@@ -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<T> {
Expand All @@ -20,20 +18,15 @@ sealed class ChildEntry<T> {
"$key@${javaClass.simpleName}"

/** All public APIs should return this type of child which is ready to work with. */
class Eager<T>(
class Initialized<T>(
override val key: RoutingKey<T>,
val node: Node,
) : ChildEntry<T>()

/** Child representation for Lazy mode. */
class Lazy<T>(
class Suspended<T>(
override val key: RoutingKey<T>,
private val resolver: Resolver<T>,
val buildContext: BuildContext,
) : ChildEntry<T>() {
internal fun initialize(): Eager<T> =
Eager(key, resolver.resolve(key.routing, buildContext).build())
}
val savedState: SavedStateMap?,
) : ChildEntry<T>()

/** When to create child nodes? */
enum class ChildMode {
Expand All @@ -46,19 +39,10 @@ sealed class ChildEntry<T> {

}

companion object {
fun <T> create(
key: RoutingKey<T>,
resolver: Resolver<T>,
buildContext: BuildContext,
childMode: ChildMode,
): ChildEntry<T> =
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,
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import com.bumble.appyx.core.node.Node
val <T> ChildEntry<T>.nodeOrNull: Node?
get() =
when (this) {
is ChildEntry.Eager -> node
is ChildEntry.Lazy -> null
is ChildEntry.Initialized -> node
is ChildEntry.Suspended -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,9 +24,11 @@ internal class ChildNodeCreationManager<Routing : Any>(
private val _children =
MutableStateFlow<Map<RoutingKey<Routing>, ChildEntry<Routing>>>(emptyMap())
val children: StateFlow<ChildEntryMap<Routing>> = _children.asStateFlow()
private lateinit var parentNode: ParentNode<Routing>

fun launch(parentNode: ParentNode<Routing>) {
savedStateMap.restoreChildren(parentNode)?.also { restoredMap ->
this.parentNode = parentNode
savedStateMap.restoreChildren()?.also { restoredMap ->
_children.update { restoredMap }
savedStateMap = null
}
Expand All @@ -40,11 +43,10 @@ internal class ChildNodeCreationManager<Routing : Any>(
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 ->
Expand All @@ -56,47 +58,51 @@ internal class ChildNodeCreationManager<Routing : Any>(
}
}

fun childOrCreate(routingKey: RoutingKey<Routing>): ChildEntry.Eager<Routing> {
@Suppress("ForbiddenComment")
fun childOrCreate(routingKey: RoutingKey<Routing>): ChildEntry.Initialized<Routing> {
// 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<Routing>): ChildEntryMap<Routing>? =
private fun SavedStateMap?.restoreChildren(): ChildEntryMap<Routing>? =
(this?.get(KEY_CHILDREN_STATE) as? Map<RoutingKey<Routing>, 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<Routing>): BuildContext =
BuildContext(
ancestryInfo = AncestryInfo.Child(parentNode),
savedStateMap = this,
customisations = customisations.getSubDirectoryOrSelf(parentNode::class)
)

fun saveChildrenState(writer: MutableSavedStateMap) {
val children = _children.value
if (children.isNotEmpty()) {
val childrenState =
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()) {
Expand All @@ -105,6 +111,29 @@ internal class ChildNodeCreationManager<Routing : Any>(
}
}

private fun childBuildContext(savedState: SavedStateMap?): BuildContext =
BuildContext(
ancestryInfo = AncestryInfo.Child(parentNode),
savedStateMap = savedState,
customisations = customisations.getSubDirectoryOrSelf(parentNode::class),
)

private fun childEntry(
key: RoutingKey<Routing>,
savedState: SavedStateMap?,
suspended: Boolean,
): ChildEntry<Routing> =
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"
}
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ abstract class ParentNode<Routing : Any>(
manageTransitions()
}

fun childOrCreate(routingKey: RoutingKey<Routing>): ChildEntry.Eager<Routing> =
fun childOrCreate(routingKey: RoutingKey<Routing>): ChildEntry.Initialized<Routing> =
childNodeCreationManager.childOrCreate(routingKey)

@Composable
fun PermanentChild(
routing: Routing,
decorator: @Composable (child: ChildRenderer) -> Unit
) {
var child by remember { mutableStateOf<ChildEntry.Eager<*>?>(null) }
var child by remember { mutableStateOf<ChildEntry.Initialized<*>?>(null) }
LaunchedEffect(routing) {
permanentNavModel.elements.collect { elements ->
val routingKey = elements.find { it.key.routing == routing }?.key
Expand Down

0 comments on commit 2348921

Please sign in to comment.