Component Lifecycle and Hooks
This page covers how to convert existing code from lifecycle methods in the Specs API to hooks in the Kotlin API.
For information on the concept and rules for hooks, see the Introduction to Hooks page.
One of the biggest changes in the Litho Kotlin API is the introduction of hooks: most of the APIs implemented with lifecycle methods in the Specs API (such as @OnCreateInitialState
and @OnAttached
) have hooks equivalents in the Kotlin API.
@State
, @OnCreateInitialState
and @OnUpdateState
β
useState
enables a component to persist a variable across renders and replaces the @State
, @OnCreateInitialState
, and @OnUpdateState
APIs. The following code shows a comparison of Spec API and Kotlin API code for the same component:
- Kotlin API
- Spec API
class CheckboxComponent : KComponent() {
override fun ComponentScope.render(): Component {
val isChecked = useState { false }
return Column(style = Style.onClick { isChecked.update { currValue -> !currValue } }) {
child(
Image(
drawable =
drawableRes(
if (isChecked.value) {
android.R.drawable.checkbox_on_background
} else {
android.R.drawable.checkbox_off_background
})))
}
}
}
@LayoutSpec
public class CheckboxComponentSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c, @State boolean isChecked) {
return Column.create(c)
.child(
Image.create(c)
.drawableRes(
isChecked
? android.R.drawable.checkbox_on_background
: android.R.drawable.checkbox_off_background))
.clickHandler(CheckboxComponent.onCheckboxClicked(c))
.build();
}
@OnUpdateState
static void updateCheckbox(StateValue<Boolean> isChecked) {
isChecked.set(!isChecked.get());
}
@OnEvent(ClickEvent.class)
static void onCheckboxClicked(ComponentContext c) {
CheckboxComponent.updateCheckbox(c);
}
}
Side-effects in @OnCreateInitialState
β
A common pattern in the Spec API was to perform side-effects in @OnCreateInitialState
(such as attaching a listener). Instead, any side-effects like this should now be done with the useEffect hook, which provides functionality to clean up and handle prop changes.
For more information, see the useState page in the 'Main Concepts' section.
@OnAttached
/@OnDetached
β
The useEffect
hook gives the ability to safely perform side-effects from a component and corresponds to the Spec API's @OnAttached
/@OnDetached
lifecycle methods.
useEffect(username) {
println("I've been attached with prop $username!")
onCleanup { println("I've been detached with prop $username!") }
}
In addition to the existing @OnAttached
/@OnDetached
functionality, it provides the ability to perform side-effects in response to changes in committed props or state. Both the effect lambda and the cleanup lambda are invoked on the main thread, meaning it's safe to perform UI-thread confined side-effects.
Changes to Attach/Detach Contractβ
In the Spec API, @OnAttached
/@OnDetached
would be called only once, when the component was first attached to/detached from the tree. This could lead to bugs where, for example, when a component subscribes to a data store using a userId prop, and then that prop changes.
useEffect
tries to solve this by taking a var-arg list of dependencies: any time any of these dependencies changes, the cleanup lambda (previously @OnDetached
) will be called followed by the new effect lambda. If no dependencies are provided, the previous cleanup and new effect lambdas will be invoked again every time a new layout is committed.
Listening to Prop/State Changesβ
An important functionality that useEffect
adds over @OnAttached
/@OnDetached
is the ability to trigger code when props/state change. For example, this can be used to trigger an animation as a side-effect whenever a prop changes:
class AnimatingCounter(private val count: Int) : KComponent() {
override fun ComponentScope.render(): Component? {
val translationY = useBinding(0f)
useEffect(count) {
// Animate the text to a Y-offset based on count
val animation = Animated.spring(translationY, to = count * 10.dp.toPixels().toFloat())
animation.start()
onCleanup { animation.cancel() }
}
return Text(style = Style.translationY(translationY), text = "$count", textSize = 24.sp)
}
}
For more information, see the useEffect page in the 'Main Concepts' section.
Lazy Stateβ
The useRef
hook allows a component to maintain a mutable reference that doesn't trigger a re-render when updated.
It returns an instance of Ref
that has a single mutable value
property, which should only be read/written on the UI thread. useRef
can be used as a replacement for lazy state from the Spec API, though it has additional uses.
Example: logging 'seen' stateβ
class LogOnceComponent : KComponent() {
override fun ComponentScope.render(): Component {
val hasLoggedVisible = useRef<Boolean> { false }
return Text(
style =
Style.onVisible {
// onVisible executes on the main thread, so it's safe to read/write hasLoggedVisible
if (!hasLoggedVisible.value) {
doLogVisible(androidContext)
hasLoggedVisible.value = true
}
},
text = "I'll let you know when I'm visible, but only once!")
}
}
For more information, see the useRef page in the 'Main Concepts' section.
@OnError
β
The useErrorBoundary
hook allows KComponents to catch and handle errors higher up in the tree and provide appropriate fallback, logging or retry mechanisms. It corresponds to the Spec API's @OnError
functionality.
Example: providing an 'error' state in the UIβ
A KComponent becomes an error boundary when it declares a useErrorBoundary
hook. The example below shows how the implementation of a boundary that renders an error state compares between the Spec API and the Kotlin API:
- Kotlin API
- Spec API
class KErrorBoundary(private val childComponent: Component) : KComponent() {
override fun ComponentScope.render(): Component? {
val errorState = useState<Exception?> { null }
useErrorBoundary { exception: Exception -> errorState.update(exception) }
errorState.value?.let {
return Column(style = Style.margin(all = 16.dp)) {
child(KDebugComponent(message = "KComponent's Error Boundary", throwable = it))
}
}
return childComponent
}
}
@LayoutSpec
public class ErrorBoundarySpec {
@OnCreateLayout
static Component onCreateLayout(
ComponentContext c, @Prop Component child, @State @Nullable Exception error) {
if (error != null) {
return DebugErrorComponent.create(c).message("Error Boundary").throwable(error).build();
}
return child;
}
@OnUpdateState
static void updateError(StateValue<Exception> error, @Param Exception e) {
error.set(e);
}
@OnError
static void onError(ComponentContext c, Exception error) {
ErrorBoundary.updateErrorSync(c, error);
}
}
For more information, see the useErrorBoundary page in the 'Main Concepts' section.
@OnCreateTransition
β
useTransition
registers a transition to be applied when the current layout is committed. It corresponds to the Spec API's @OnCreateTransition
functionality.
- Kotlin API
- Spec API
class TransitionComponent : KComponent() {
override fun ComponentScope.render(): Component {
val isHalfAlpha = useState { false }
useTransition(Transition.create("square").animate(AnimatedProperties.ALPHA))
return Column(style = Style.onClick { isHalfAlpha.update(!isHalfAlpha.value) }) {
child(
Row(
style =
Style.backgroundColor(Color.YELLOW)
.width(80.dp)
.height(80.dp)
.alpha(
if (isHalfAlpha.value) {
0.5f
} else {
1.0f
})
.transitionKey(context, "square")))
}
}
}
@LayoutSpec
public class AlphaTransitionComponentSpec {
private static final String SQUARE_KEY = "square";
@OnCreateLayout
static Component onCreateLayout(ComponentContext c, @State boolean isHalfAlpha) {
return Column.create(c)
.clickHandler(AlphaTransitionComponent.onClickEvent(c))
.child(
Row.create(c)
.backgroundColor(Color.YELLOW)
.widthDip(80)
.heightDip(80)
.alpha(isHalfAlpha ? 0.5f : 1.0f)
.transitionKey(SQUARE_KEY))
.build();
}
@OnCreateTransition
static Transition onCreateTransition(ComponentContext c) {
return Transition.create(SQUARE_KEY).animate(AnimatedProperties.ALPHA);
}
@OnEvent(ClickEvent.class)
static void onClickEvent(ComponentContext c, @FromEvent View view) {
AlphaTransitionComponent.onUpdateState(c);
}
@OnUpdateState
static void onUpdateState(StateValue<Boolean> isHalfAlpha) {
isHalfAlpha.set(!isHalfAlpha.get());
}
}