Appyx Interactions – Operations¶
Overview¶
A TransitionModel
behaves like a state machine. If you want a new ModelState
to be created, you must pass it an instance of an Operation
.
Operation¶
Operation defines a (ModelState) -> ModelState
change.
interface Operation<ModelState> {
val mode: Mode
fun isApplicable(state: ModelState): Boolean
operator fun invoke(state: ModelState): StateTransition<ModelState>
}
Operation.Mode¶
A mode maybe specified by client code to define how the TransitionModel
should handle the state change with regards to interrupts.
interface Operation<ModelState> {
enum class Mode {
IMMEDIATE, KEYFRAME
}
}
You can test their effects on the below sample. Try spamming the buttons:
In code, both buttons invoke the same operation, only with different modes:
{ testDrive.next(mode = KEYFRAME) }
// vs
{ testDrive.next(mode = IMMEDIATE, animationSpec = spring(
stiffness = Spring.StiffnessVeryLow,
dampingRatio = Spring.DampingRatioMediumBouncy
)) }
In KEYFRAME
mode the current transition isn’t interrupted, and it will be guaranteed to finish before any additional transitions. New states are enqueued for execution afterwards. The overall execution progress speeds up proportionally to the size of enqueued operations.
In IMMEDIATE
mode the current transition is interrupted and will not be finished. New operations overwrite the current target state, resulting in the UI dynamically changing course always towards the latest one.
Applicability check¶
fun isApplicable(state: ModelState): Boolean
This method is used for checks without actually executing an Operation
.
Invoking the Operation¶
This method should create a StateTransition<ModelState>
:
operator fun invoke(state: ModelState): StateTransition<ModelState>
Which is defined as:
data class StateTransition<ModelState>(
val fromState: ModelState,
val targetState: ModelState
)
It is imperative that:
fromState
is derived from the baseline state (the one passed toinvoke
)fromState
adds any new elements to the state that should be animated in and are not contained in the baseline statetargetState
is derived fromfromState
and never directly from the baseline state
To make it easier, it is recommended to extend BaseOperation
which helps to enforce the above:
abstract class BaseOperation<ModelState> : Operation<ModelState> {
final override fun invoke(baseLineState: ModelState): StateTransition<ModelState> {
val fromState = createFromState(baseLineState)
val targetState = createTargetState(fromState)
return StateTransition(
fromState = fromState,
targetState = targetState
)
}
/**
* @return ModelState If the operation adds any new elements to the scene,
* it MUST add them to the state here.
* Otherwise, just return baseLineState unchanged.
*/
abstract fun createFromState(baseLineState: ModelState): ModelState
/**
* @return ModelState Any elements present in this state
* MUST also be present in the fromState already.
*/
abstract fun createTargetState(fromState: ModelState): ModelState
}
Examples¶
You can find examples for implementing operations in the repo. Some possible points of interest:
TestDrive
/MoveTo
BackStack
/Push
,Pop
,Replace
Spotlight
/Next
,Prev
,First
,Last
Notice how each of them also comes with their convenient extension methods that allow these operations to be invoked directly on their corresponding AppyxComponent
, so that client code can use a simple API:
// Without extension methods
testDrive.accept(MoveTo(A))
backStack.accept(Pop())
spotlight.accept(Last())
// With extension methods
testDrive.moveTo(A)
backStack.pop()
spotlight.last()