Skip to content

Commit

Permalink
Merge pull request #694 from manuel-martos/2.x-fix-component-state-no…
Browse files Browse the repository at this point in the history
…t-restored

AppyxComponent state not restored after configuration changes
  • Loading branch information
manuel-martos authored Apr 12, 2024
2 parents 0f65b2d + b54d891 commit a9a46ed
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [#670](https://github.com/bumble-tech/appyx/pull/670) - Fixes ios lifecycle
- [#673](https://github.com/bumble-tech/appyx/pull/673) – Fix canHandeBackPress typo
- [#671](https://github.com/bumble-tech/appyx/issue/671) – Fix ui state saving issue
- [#694](https://github.com/bumble-tech/appyx/pull/694) – Fix appyxComponent state saving issue

### Enhancement

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.bumble.appyx.helpers

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.spring
import com.bumble.appyx.helpers.DummyComponentModel.State
import com.bumble.appyx.interactions.gesture.GestureFactory
import com.bumble.appyx.interactions.gesture.GestureSettleConfig
import com.bumble.appyx.interactions.model.BaseAppyxComponent
import com.bumble.appyx.interactions.model.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.interactions.model.backpresshandlerstrategies.DontHandleBackPress
import com.bumble.appyx.interactions.state.MutableSavedStateMap
import com.bumble.appyx.interactions.ui.Visualisation
import com.bumble.appyx.interactions.ui.context.TransitionBounds
import com.bumble.appyx.interactions.ui.context.UiContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

class DummyComponent<NavTarget : Any>(
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
val model: DummyComponentModel<NavTarget>,
visualisation: (UiContext) -> Visualisation<NavTarget, State<NavTarget>>,
animationSpec: AnimationSpec<Float> = spring(),
gestureFactory: (TransitionBounds) -> GestureFactory<NavTarget, State<NavTarget>> = {
GestureFactory.Noop()
},
gestureSettleConfig: GestureSettleConfig = GestureSettleConfig(),
backPressStrategy: BackPressHandlerStrategy<NavTarget, State<NavTarget>> = DontHandleBackPress(),
disableAnimations: Boolean = false,
) : BaseAppyxComponent<NavTarget, State<NavTarget>>(
scope = scope,
model = model,
visualisation = visualisation,
gestureFactory = gestureFactory,
gestureSettleConfig = gestureSettleConfig,
backPressStrategy = backPressStrategy,
defaultAnimationSpec = animationSpec,
disableAnimations = disableAnimations
) {
var saveInstanceStateInvoked: Int = 0

fun resetSaveInstanceState() {
saveInstanceStateInvoked = 0
}

override fun saveInstanceState(state: MutableSavedStateMap) {
super.saveInstanceState(state)
saveInstanceStateInvoked += 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bumble.appyx.helpers

import com.bumble.appyx.helpers.DummyComponentModel.State
import com.bumble.appyx.interactions.model.Element
import com.bumble.appyx.interactions.model.asElement
import com.bumble.appyx.interactions.model.transition.BaseTransitionModel
import com.bumble.appyx.utils.multiplatform.Parcelable
import com.bumble.appyx.utils.multiplatform.Parcelize
import com.bumble.appyx.utils.multiplatform.SavedStateMap

class DummyComponentModel<NavTarget : Any>(
initialTarget: NavTarget,
savedStateMap: SavedStateMap?,
) : BaseTransitionModel<NavTarget, State<NavTarget>>(
savedStateMap = savedStateMap,
) {
@Parcelize
data class State<NavTarget>(
val target: Element<NavTarget>
) : Parcelable

override val initialState: State<NavTarget> = State(initialTarget.asElement())

override fun State<NavTarget>.availableElements(): Set<Element<NavTarget>> = setOf(target)

override fun State<NavTarget>.removeDestroyedElement(element: Element<NavTarget>): State<NavTarget> =
this

override fun State<NavTarget>.removeDestroyedElements(): State<NavTarget> = this

override fun State<NavTarget>.destroyedElements(): Set<Element<NavTarget>> = emptySet()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bumble.appyx.helpers

import androidx.compose.animation.core.SpringSpec
import com.bumble.appyx.helpers.DummyComponentModel.State
import com.bumble.appyx.interactions.ui.DefaultAnimationSpec
import com.bumble.appyx.interactions.ui.context.UiContext
import com.bumble.appyx.interactions.ui.state.MatchedTargetUiState
import com.bumble.appyx.transitionmodel.BaseVisualisation

class DummyVisualisation<NavTarget : Any>(
uiContext: UiContext,
defaultAnimationSpec: SpringSpec<Float> = DefaultAnimationSpec
) : BaseVisualisation<NavTarget, State<NavTarget>, TargetUiState, MutableUiState>(
uiContext = uiContext,
defaultAnimationSpec = defaultAnimationSpec,
) {
override fun State<NavTarget>.toUiTargets(): List<MatchedTargetUiState<NavTarget, TargetUiState>> =
listOf(
MatchedTargetUiState(
element = target,
targetUiState = TargetUiState(0)
)
)

override fun mutableUiStateFor(
uiContext: UiContext,
targetUiState: TargetUiState
): MutableUiState =
targetUiState.toMutableUiState(uiContext)


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.bumble.appyx.helpers

import androidx.compose.material.Text
import com.bumble.appyx.helpers.RootNode.NavTarget
import com.bumble.appyx.navigation.modality.NodeContext
import com.bumble.appyx.navigation.node.Node
import com.bumble.appyx.navigation.node.node

class RootNode(
nodeContext: NodeContext,
appyxComponent: DummyComponent<NavTarget> = DummyComponent(
model = DummyComponentModel(
initialTarget = NavTarget.Child1,
savedStateMap = nodeContext.savedStateMap
),
visualisation = { DummyVisualisation(it) }
)
) : Node<NavTarget>(
nodeContext = nodeContext,
appyxComponent = appyxComponent,
) {
sealed interface NavTarget {
data object Child1 : NavTarget
data object Child2 : NavTarget
}

override fun buildChildNode(navTarget: NavTarget, nodeContext: NodeContext): Node<*> =
when (navTarget) {
is NavTarget.Child1 -> node(nodeContext) { Text("Child 1") }
is NavTarget.Child2 -> node(nodeContext) { Text("Child 2") }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.bumble.appyx.helpers

import androidx.compose.animation.core.SpringSpec
import androidx.compose.ui.Modifier
import com.bumble.appyx.interactions.ui.context.UiContext
import com.bumble.appyx.interactions.ui.state.BaseMutableUiState
import kotlinx.coroutines.CoroutineScope

class TargetUiState(
val id: Int,
)

class MutableUiState(
uiContext: UiContext,
val id: Int,
) : BaseMutableUiState<TargetUiState>(
uiContext, emptyList()
) {
override val combinedMotionPropertyModifier: Modifier = Modifier

override suspend fun snapTo(target: TargetUiState) = Unit

override fun lerpTo(
scope: CoroutineScope,
start: TargetUiState,
end: TargetUiState,
fraction: Float
) = Unit

override suspend fun animateTo(
scope: CoroutineScope,
target: TargetUiState,
springSpec: SpringSpec<Float>
) = Unit
}

fun TargetUiState.toMutableUiState(uiContext: UiContext): MutableUiState =
MutableUiState(
uiContext = uiContext,
id = id,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.bumble.appyx.navigation.node

import com.bumble.appyx.helpers.DummyComponent
import com.bumble.appyx.helpers.DummyComponentModel
import com.bumble.appyx.helpers.DummyVisualisation
import com.bumble.appyx.helpers.RootNode
import com.bumble.appyx.navigation.AppyxTestScenario
import com.bumble.appyx.navigation.modality.NodeContext
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test

class NodeTest {
private var appyxComponent: DummyComponent<RootNode.NavTarget>? = null

private val nodeFactory: (nodeContext: NodeContext) -> Node<*> = { nodeContext ->
appyxComponent = DummyComponent(
model = DummyComponentModel(
initialTarget = RootNode.NavTarget.Child1,
savedStateMap = nodeContext.savedStateMap
),
visualisation = { DummyVisualisation(it) }
)
RootNode(nodeContext = nodeContext, appyxComponent = appyxComponent!!)
}

@get:Rule
val rule = AppyxTestScenario { nodeContext ->
nodeFactory(nodeContext)
}

@Test
fun WHEN_node_is_create_THEN_plugins_are_setup_as_expected() {
rule.start()
assertNotEquals("Node should have some predefined plugins", 0, rule.node.plugins.size)
}

@Test
fun WHEN_node_is_create_THEN_appyx_component_state_is_saved_during_recreation() {
rule.start()
appyxComponent!!.resetSaveInstanceState()
rule.activityScenario.recreate()
assertNotEquals(
"AppyxComponent state should be saved",
0,
appyxComponent!!.saveInstanceStateInvoked
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ abstract class Node<NavTarget : Any>(
val id: String
get() = nodeContext.identifier

val plugins: List<Plugin> = plugins + listOfNotNull(this as? Plugin)
val plugins: List<Plugin> = plugins + appyxComponent + childAware + listOfNotNull(this as? Plugin)

val ancestryInfo: AncestryInfo =
nodeContext.ancestryInfo
Expand Down

0 comments on commit a9a46ed

Please sign in to comment.