Skip to main content

Transitions

Transitions are an important concept in Litho, and the Kotlin API provides a set of powerful hooks for transition support. This makes it possible to replicate the behaviors of both @OnCreateTransition and @OnUpdateStateWithTransition in Kotlin.

useTransition APIs​

useTransition registers a transition (or set of transitions) to be applied when the current layout is committed. Two variants of this API are available for different use-cases.

When a transition is unconditional, and does not have any dependencies, such case can be represented by the simple and efficient useTransition API. This corresponds to the simplest use-case of the Spec API's @OnCreateTransition.

class AlphaTransitionKComponent : KComponent() {

override fun ComponentScope.render(): Component {
val isHalfAlpha = useState { false }
useTransition(Transition.create(SQUARE_KEY).animate(AnimatedProperties.ALPHA))
return Column(style = Style.onClick { isHalfAlpha.update { !it } }) {
child(
Row(
style =
Style.transitionKey(context, SQUARE_KEY, Transition.TransitionKeyType.GLOBAL)
.backgroundColor(Color.YELLOW)
.width(80.dp)
.height(80.dp)
.alpha(if (isHalfAlpha.value) 0.5f else 1.0f)))
}
}
}

However, for more complex use-cases where the transition is directly anchored to a set of dependencies. These may be states, props, or derived values. In such case, the more powerful useTransition with dependency API may be used. This API makes it possible to re-evaluate the transition whenever any of the dependencies change. It also provides access to the previous and next values of the declared dependency so that they may participate in the evaluation of the resulting transition. This ensures full parity with all variations of @OnCreateTransition as well as the transition part of @OnUpdateStateWithTransition

val state = useState { TriState.HEIGHT }
useTransition(state.value) {
val (previous, next) = diffOf(state.value)
val animator =
when (if (previous == null || next == null) 0 else previous.ordinal + next.ordinal) {
1 -> Transition.SPRING_WITH_OVERSHOOT
2 -> Transition.timing(1_000, AccelerateDecelerateInterpolator())
3 -> Transition.springWithConfig(250.0, 10.0)
else -> Transition.timing(0)
}
Transition.create(Transition.TransitionKeyType.GLOBAL, "fancy-component")
.animate(*AnimatedProperties.AUTO_LAYOUT_PROPERTIES)
.animator(animator)
}

Which useTransition to use when?​

Scenariosimple useTransitionuseTransition with dependencies
The same transition is always applied unconditionallyβœ…
Transition only needs access to the current value of state, prop or derived valueβœ…
Transition may change depending on some valueβœ…
Transition is only applied whenever specific value changesβœ…
Resulting transition depends on the previous and/or next value of some dependencyβœ…
override fun ComponentScope.render(): Component {
val number = useState { 0 }
val delta = useBinding(0)
useTransition(number.value) {
val (previous, next) = diffOf(number.value)
val d = if (previous == null || next == null) next ?: 0 else next - previous
delta.set(d)
Transition.create(Transition.TransitionKeyType.GLOBAL, "bubble")
.animate(AnimatedProperties.SCALE)
.animator(Transition.springWithConfig(250.0, 10.0))
}
val text = buildString {
val d = delta.get()
if (d == 0) append("Tap me") else append(if (d > 0) "+" else "-").append(d.absoluteValue)
}
return Text(
text,
alignment = TextAlignment.CENTER,
verticalGravity = VerticalGravity.CENTER,
style =
Style.width(100.dp)
.height(100.dp)
.alignSelf(YogaAlign.CENTER)
.transitionKey(context, "bubble", Transition.TransitionKeyType.GLOBAL)
.margin(all = 10.dp)
.scale(lerp((number.value - MIN) / (MAX - MIN).toFloat(), 0.5f, 1.5f))
.background(RoundedRect(0xff6ab071, 8.dp))
.onClick { number.update(Random.nextInt(MIN, MAX)) })
}

Migrating from @OnCreateTransition/@OnUpdateStateWithTransition​

The table below shows a comparison of different scenarios implemented via Spec-Gen API and their equivalent Kotlin API

Scenarios​

Simple transition without parameters​

Spec-Gen APIKotlin API
@OnCreateTransition
fun createTransition(context: ComponentContext): Transition {
return Transition.allLayout()
}
useTransition(Transition.allLayout())

Transition needs only current value of state/prop/derived value​

Spec-Gen APIKotlin API
@OnCreateTransition
fun createTransition(
context: ComponentContext
@State someEnum: EnumType
): Transition {
return when(someEnum) {
EnumType.VALUE_1 -> Transition.parallel(...)
EnumType.VALUE_2 -> Transition.create(...)
else -> null
}
useTransition(
when(someEnum) {
EnumType.VALUE_1 -> Transition.parallel(...)
EnumType.VALUE_2 -> Transition.create(...)
else -> null
}
)

Evaluate transition only if state has changed​

Spec-Gen APIKotlin API
@OnCreateTransition
fun createTransition(
context: ComponentContext,
@State isEnabled: Diff<Boolean>
): Transition {
val hasStateChanged = isEnabled.previous != isEnabled.next
return if (hasStateChanged) {
Transition.allLayout()
} else {
null
}
}
useTransition(isEnabled) {
Transition.allLayout()
}

Dependency change from specific value to another​

Spec-Gen APIKotlin API
@OnCreateTransition
fun onCreateTransition(
c: ComponentContext,
@Prop storyContext: StoryContext,
@State hidden: Diff<Boolean>,
@State collapsed: Diff<Boolean>,
): Transition? {
if (collapsed.previous == false && collapsed.next == true) {
return …
}
if (collapsed.previous == true && collapsed.next == false) {
return …
}
if (hidden.previous == false && hidden.next == true) {
return …
}
...
return null
}
useTransition(storyContext, hidden, collapsed) {
val hidden = diffOf(hidden)
val collapsed = diffOf(collapsed)
if (collapsed.previous == false && collapsed.next == true) {
return@useTransition …
}
if (collapsed.previous == true && collapsed.next == false) {
return@useTransition …
}
if (hidden.previous == false && hidden.next == true) {
return@useTransition …
}
...
null
}

Another complex use-case​

Spec-Gen APIKotlin API
@OnCreateTransition
fun onCreateTransition(
c: ComponentContext,
@Prop playerType: PlayerType,
@State visibility: Diff<Visibility>
): Transition {
when {
visibility.previous == INVISIBLE -> return ...

visibility.next == INVISIBLE -> return ...

visibility.previous == PARTLY_VISIBLE &&
visibility.next == VISIBLE -> return ...

visibility.previous == VISIBLE &&
visibility.next == PARTLY_VISIBLE -> return ...

else -> return ...
}
}
useTransition(playerType, visibility) {
val visibility = diffOf(visibility)
when {
visibility.previous == INVISIBLE -> Transition(...)

visibility.next == INVISIBLE -> Transition(...)

visibility.previous == PARTLY_VISIBLE &&
visibility.next == VISIBLE -> Transition(...)

visibility.previous == VISIBLE &&
visibility.next == PARTLY_VISIBLE -> Transition(...)

else -> Transition(...)
}
}

Interplay of state and transition​

Spec-Gen APIKotlin API
@OnEvent(ClickEvent::class)
fun triggerClickEvent(c: ComponentContext) {
SomeComponent.onUpdateShiftWithTransition(c, newShiftValue)
}

@OnUpdateStateWithTransition
fun onUpdateShift(
shiftPx: StateValue<Int>,
newShiftValue: Int
): Transition {
shiftPx.set(newShiftValue)
return Transition.create(SHIFT_TRANSITION_KEY)
.animate(AnimatedProperties.Y)
}
fun ComponentScope.render() {
...
useTransition(shiftPx) {
Transition.create(SHIFT_TRANSITION_KEY)
.animate(AnimatedProperties.Y)
}
...
return SomeComponent(
style = Style.onClick { shiftPx.update(newShiftValue) })
}