Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AppyxComponent state not restored after configuration changes #694

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lateinit var can be used


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