Skip to content

Commit

Permalink
- Added shared element example without movable content
Browse files Browse the repository at this point in the history
- Moved LocalMovableContentMap to Children composable and added a parameter to it.
- Added support for more movableContent API variants
  • Loading branch information
KovalevAndrey committed May 7, 2024
1 parent 695b88c commit 76d6c1f
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 97 deletions.
2 changes: 1 addition & 1 deletion libraries/core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalMovableContentMap</ID>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalNodeTargetVisibility</ID>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalParentNodeMovableContent</ID>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalSharedElementScope</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.bumble.appyx.core.navigation.transition.TransitionBounds
import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
import com.bumble.appyx.core.navigation.transition.TransitionHandler
import com.bumble.appyx.core.navigation.transition.TransitionParams
import com.bumble.appyx.core.node.LocalMovableContentMap
import com.bumble.appyx.core.node.LocalNodeTargetVisibility
import com.bumble.appyx.core.node.LocalSharedElementScope
import com.bumble.appyx.core.node.ParentNode
Expand All @@ -41,6 +42,7 @@ inline fun <reified NavTarget : Any, State> ParentNode<NavTarget>.Children(
modifier: Modifier = Modifier,
transitionHandler: TransitionHandler<NavTarget, State> = remember { JumpToEndTransitionHandler() },
withSharedElementTransition: Boolean = false,
withMovableContent: Boolean = false,
noinline block: @Composable ChildrenTransitionScope<NavTarget, State>.() -> Unit = {
children<NavTarget> { child ->
child()
Expand All @@ -67,7 +69,8 @@ inline fun <reified NavTarget : Any, State> ParentNode<NavTarget>.Children(
) {
CompositionLocalProvider(
/** LocalSharedElementScope will be consumed by children UI to apply shareElement modifier */
LocalSharedElementScope provides this
LocalSharedElementScope provides this,
LocalMovableContentMap provides if (withMovableContent) mutableMapOf() else null
) {
block(
ChildrenTransitionScope(
Expand All @@ -87,7 +90,8 @@ inline fun <reified NavTarget : Any, State> ParentNode<NavTarget>.Children(
CompositionLocalProvider(
/** If sharedElement is not supported for this Node - provide null otherwise children
* can consume ascendant's LocalSharedElementScope */
LocalSharedElementScope provides null
LocalSharedElementScope provides null,
LocalMovableContentMap provides if (withMovableContent) mutableMapOf() else null
) {
block(
ChildrenTransitionScope(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package com.bumble.appyx.core.navigation.transition

import androidx.compose.runtime.Composable
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.movableContentWithReceiverOf
import com.bumble.appyx.core.node.LocalMovableContentMap
import com.bumble.appyx.core.node.LocalNodeTargetVisibility
import com.bumble.appyx.core.node.LocalParentNodeMovableContent


/**
* Returns movable content for the given key. To reuse composable across Nodes movable content
* must to be invoked only once at any time, therefore we should return null for if the Node is
* transitioning to an invisible target and return movable content only if the targetState is visible.
* must to be invoked only once at any time, therefore we should return null for the case when the
* Node is transitioning to an invisible target and return movable content only if the targetState
* is visible.
*
* Example: ParentNode (P) has BackStack with one Active Node (A). We push Node (B) to the BackStack,
* and we want to move content from Node (A) to Node (B). Node (A) is transitioning from Active to
Expand All @@ -23,14 +25,162 @@ import com.bumble.appyx.core.node.LocalParentNodeMovableContent
@Composable
fun localMovableContentWithTargetVisibility(
key: Any,
defaultValue: @Composable () -> Unit
content: @Composable () -> Unit
): (@Composable () -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = LocalParentNodeMovableContent.current
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentOf {
defaultValue()
content()
}
} as? @Composable () -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <P> localMovableContentWithTargetVisibility(
key: Any,
content: @Composable (P) -> Unit
): (@Composable (P) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentOf<P> {
content(it)
}
} as? @Composable (P) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}


@Composable
fun <P1, P2> localMovableContentWithTargetVisibility(
key: Any,
content: @Composable (P1, P2) -> Unit
): (@Composable (P1, P2) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentOf<P1, P2> { p1, p2 ->
content(p1, p2)
}
} as? @Composable (P1, P2) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <P1, P2, P3> localMovableContentWithTargetVisibility(
key: Any,
content: @Composable (P1, P2, P3) -> Unit
): (@Composable (P1, P2, P3) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentOf<P1, P2, P3> { p1, p2, p3 ->
content(p1, p2, p3)
}
} as? @Composable (P1, P2, P3) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <P1, P2, P3, P4> localMovableContentWithTargetVisibility(
key: Any,
content: @Composable (P1, P2, P3, P4) -> Unit
): (@Composable (P1, P2, P3, P4) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentOf<P1, P2, P3, P4> { p1, p2, p3, p4 ->
content(p1, p2, p3, p4)
}
} as? @Composable (P1, P2, P3, P4) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <R> localMovableContentWithReceiverAndTargetVisibility(
key: Any,
content: @Composable R.() -> Unit
): (@Composable R.() -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentWithReceiverOf<R> {
this.content()
}
} as? @Composable R.() -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <R, P> localMovableContentWithReceiverAndTargetVisibility(
key: Any,
content: @Composable R.(P) -> Unit
): (@Composable R.(P) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentWithReceiverOf<R, P> { p ->
this.content(p)
}
} as? @Composable R.(P) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <R, P1, P2> localMovableContentWithReceiverAndTargetVisibility(
key: Any,
content: @Composable R.(P1, P2) -> Unit
): (@Composable R.(P1, P2) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentWithReceiverOf<R, P1, P2> { p1, p2 ->
this.content(p1, p2)
}
} as? @Composable R.(P1, P2) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
fun <R, P1, P2, P3> localMovableContentWithReceiverAndTargetVisibility(
key: Any,
content: @Composable R.(P1, P2, P3) -> Unit
): (@Composable R.(P1, P2, P3) -> Unit)? {
if (!LocalNodeTargetVisibility.current) return null
val movableContentMap = retrieveMovableContentMap()
return movableContentMap.getOrPut(key) {
movableContentWithReceiverOf<R, P1, P2, P3> { p1, p2, p3 ->
this.content(p1, p2, p3)
}
} as? @Composable R.(P1, P2, P3) -> Unit ?: throw IllegalStateException(
"Movable content for key $key is not of the expected type." +
" The same $key has been used for different types of content."
)
}

@Composable
private fun retrieveMovableContentMap(): MutableMap<Any, Any> {
return requireNotNull(LocalMovableContentMap.current) {
"LocalMovableContentMap not found in the composition hierarchy." +
" Please use withMovableContent = true on Children composable."
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.bumble.appyx.core.node

import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf

val LocalNode = compositionLocalOf<Node?> { null }
Expand All @@ -18,5 +17,7 @@ val LocalSharedElementScope = compositionLocalOf<SharedTransitionScope?> { null
*/
val LocalNodeTargetVisibility = compositionLocalOf { false }

val LocalParentNodeMovableContent =
compositionLocalOf<MutableMap<Any, @Composable () -> Unit>> { mutableMapOf() }
/**
* Represents the map from which movable content can be retrieved.
*/
val LocalMovableContentMap = compositionLocalOf<MutableMap<Any, Any>?> { null }
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,11 @@ open class Node @VisibleForTesting internal constructor(
LocalNode provides this,
LocalLifecycleOwner provides this,
) {
DerivedSetup {
HandleBackPress()
View(modifier)
}
HandleBackPress()
View(modifier)
}
}

@Composable
protected open fun DerivedSetup(innerContent: @Composable () -> Unit) {
innerContent()
}

@Composable
private fun HandleBackPress() {
// can't use BackHandler Composable because plugins provide OnBackPressedCallback which is not observable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.bumble.appyx.core.node

import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -76,15 +74,6 @@ abstract class ParentNode<NavTarget : Any>(
manageTransitions()
}

@Composable
final override fun DerivedSetup(innerContent: @Composable () -> Unit) {
CompositionLocalProvider(
LocalParentNodeMovableContent provides mutableMapOf()
) {
innerContent()
}
}

fun childOrCreate(navKey: NavKey<NavTarget>): ChildEntry.Initialized<NavTarget> =
childNodeCreationManager.childOrCreate(navKey)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.MviCore
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.MviCoreLeafExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.NavModelExamples
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.Picker
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementFaderExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementWithMovableContentExample
import com.bumble.appyx.sandbox.client.customisations.CustomisationsNode
import com.bumble.appyx.sandbox.client.explicitnavigation.ExplicitNavigationExampleActivity
import com.bumble.appyx.sandbox.client.integrationpoint.IntegrationPointExampleNode
Expand All @@ -48,7 +49,7 @@ import com.bumble.appyx.sandbox.client.list.LazyListContainerNode
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleBuilder
import com.bumble.appyx.sandbox.client.mvicoreexample.leaf.MviCoreLeafBuilder
import com.bumble.appyx.sandbox.client.navmodels.NavModelExamplesNode
import com.bumble.appyx.sandbox.client.sharedelement.SharedElementWithMovableContentExampleNode
import com.bumble.appyx.sandbox.client.sharedelement.SharedElementExampleNode
import com.bumble.appyx.utils.customisations.NodeCustomisation
import kotlinx.parcelize.Parcelize

Expand All @@ -75,7 +76,10 @@ class ContainerNode internal constructor(
object LazyExamples : NavTarget()

@Parcelize
object SharedElementFaderExample : NavTarget()
object SharedElementExample : NavTarget()

@Parcelize
object SharedElementWithMovableContentExample : NavTarget()

@Parcelize
object IntegrationPointExample : NavTarget()
Expand All @@ -101,12 +105,21 @@ class ContainerNode internal constructor(
when (navTarget) {
is Picker -> node(buildContext) { modifier -> ExamplesList(modifier) }
is NavModelExamples -> NavModelExamplesNode(buildContext)
is SharedElementFaderExample -> SharedElementWithMovableContentExampleNode(buildContext)
is SharedElementExample -> SharedElementExampleNode(buildContext)
is SharedElementWithMovableContentExample -> SharedElementExampleNode(
buildContext,
hasMovableContent = true
)

is LazyExamples -> LazyListContainerNode(buildContext)
is IntegrationPointExample -> IntegrationPointExampleNode(buildContext)
is BlockerExample -> BlockerExampleNode(buildContext)
is Customisations -> CustomisationsNode(buildContext)
is MviCoreExample -> MviCoreExampleBuilder().build(buildContext, "MVICore initial state")
is MviCoreExample -> MviCoreExampleBuilder().build(
buildContext,
"MVICore initial state"
)

is MviCoreLeafExample -> MviCoreLeafBuilder().build(
buildContext,
"MVICore leaf initial state"
Expand Down Expand Up @@ -143,7 +156,12 @@ class ContainerNode internal constructor(
label?.let {
Text(it, textAlign = TextAlign.Center)
}
TextButton("Shared element with movable content") { backStack.push(SharedElementFaderExample) }
TextButton("Shared element ") { backStack.push(SharedElementExample) }
TextButton("Shared element with movable content") {
backStack.push(
SharedElementWithMovableContentExample
)
}
TextButton("NavModel Examples") { backStack.push(NavModelExamples) }
TextButton("Customisations Example") { backStack.push(Customisations) }
TextButton("Explicit navigation example") {
Expand Down
Loading

0 comments on commit 76d6c1f

Please sign in to comment.