Communicating Between Components
Dispatching an Event from a child to its parentβ
In the Spec API, communicating from a child to a parent is done through an EventHandler
, which handles a custom event type. The EventHandler
is defined in the parent component and passed as a Prop to the child component. For more information on Spec events, see the document Events for Specs page.
@LayoutSpec
class ParentComponentReceivesEventFromChildSpec {
@OnCreateInitialState
static void onCreateInitialState(ComponentContext c, StateValue<String> infoText) {
infoText.set("No event received from ChildComponent");
}
@OnCreateLayout
static Component onCreateLayout(
ComponentContext c, @Prop ComponentEventObserver observer, @State String infoText) {
return Column.create(c)
.paddingDip(YogaEdge.ALL, 30)
.child(Text.create(c).text("ParentComponent").textSizeDip(30))
.child(Text.create(c).text(infoText).textSizeDip(15))
.child(
ChildComponentSendsEventToParent.create(c)
.observer(observer)
.notifyParentEventHandler(
ParentComponentReceivesEventFromChild.onNotifyParentEvent(c)))
.build();
}
@OnEvent(NotifyParentEvent.class)
static void onNotifyParentEvent(ComponentContext c) {
ParentComponentReceivesEventFromChild.onUpdateInfoText(c);
}
@OnUpdateState
static void onUpdateInfoText(StateValue<String> infoText) {
infoText.set("Received event from ChildComponent!");
}
}
The child component can invoke the event handler received from the parent to inform the parent that a certain action took place, such as when the child component receives a click event or, in a visibility handler, when it becomes visible. The following code provides an example.
@LayoutSpec(events = {NotifyParentEvent.class})
class ChildComponentSendsEventToParentSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c) {
return Column.create(c)
.marginDip(YogaEdge.ALL, 30)
.child(Text.create(c).text("ChildComponent").textSizeDip(20))
.child(
Text.create(c)
.paddingDip(YogaEdge.ALL, 5)
.border(
Border.create(c)
.color(YogaEdge.ALL, Color.BLACK)
.radiusDip(2f)
.widthDip(YogaEdge.ALL, 1)
.build())
.text("Click to send event to parent!")
.textSizeDip(15)
.clickHandler(ChildComponentSendsEventToParent.onClickEvent(c)))
.child(
Text.create(c)
.paddingDip(YogaEdge.ALL, 5)
.border(
Border.create(c)
.color(YogaEdge.ALL, Color.BLACK)
.radiusDip(2f)
.widthDip(YogaEdge.ALL, 1)
.build())
.text("Click to send event to Activity!")
.textSizeDip(15)
.clickHandler(ChildComponentSendsEventToParent.onSendEventToActivity(c)))
.build();
}
@OnEvent(ClickEvent.class)
static void onClickEvent(ComponentContext c) {
ChildComponentSendsEventToParent.dispatchNotifyParentEvent(
ChildComponentSendsEventToParent.getNotifyParentEventHandler(c));
}
@OnEvent(ClickEvent.class)
static void onSendEventToActivity(ComponentContext c, @Prop ComponentEventObserver observer) {
observer.onComponentClicked();
}
}
Passing new Props from a parent to a childβ
If a parent component needs to pass new data to a child, it can do so by simply passing new props to the child component. When the data is updated as a result of an action controlled by the parent component (for example, a click event on the parent component), the new data is passed down to the child component by triggering a 'state update', which updates the value of the prop that will be passed to the child component and recreates the child with this new value. The child component receives the latest value of the state through the prop when it's created.
The following code illustrates this concept with a click event on the parent component.
.child(
Text.create(c)
.paddingDip(YogaEdge.ALL, 5)
.text("Click to send new text to ChildComponent")
.border(
Border.create(c)
.color(YogaEdge.ALL, Color.BLACK)
.radiusDip(2f)
.widthDip(YogaEdge.ALL, 1)
.build())
.textSizeDip(15)
.clickHandler(ParentComponentSendsEventToChild.onClickCounter(c)))
.child(
ChildComponentReceivesEventFromParent.create(c)
.textFromParent("Version " + counterForChildComponentText))
.build();
}
@OnEvent(ClickEvent.class)
static void onClickCounter(ComponentContext c) {
ParentComponentSendsEventToChild.onUpdateCounterForChildComponent(c);
}
@OnUpdateState
static void onUpdateCounterForChildComponent(StateValue<Integer> counterForChildComponentText) {
counterForChildComponentText.set(counterForChildComponentText.get() + 1);
}
Triggering an Action on a child from a parentβ
There are cases when a parent needs to trigger an action on a child instead of just passing new data. To do this, the parent needs to keep a reference to the child and trigger an action on it using that reference.
The reference to the child is maintained through a Handle
instance, which the parent creates and passes to the child component as a prop:
@OnCreateInitialState
static void onCreateInitialState(ComponentContext c, final StateValue<Handle> childHandle) {
childHandle.set(new Handle());
}
@OnCreateLayout
static Component onCreateLayout(
ComponentContext c, @State int counterForChildComponentText, @State Handle childHandle) {
return Column.create(c)
.paddingDip(YogaEdge.ALL, 30)
.child(Text.create(c).text("ParentComponent").textSizeDip(30))
.child(
Text.create(c)
.paddingDip(YogaEdge.ALL, 5)
.text("Click to trigger show toast event on ChildComponent with handle")
.marginDip(YogaEdge.TOP, 15)
.border(
Border.create(c)
.color(YogaEdge.ALL, Color.BLACK)
.radiusDip(2f)
.widthDip(YogaEdge.ALL, 1)
.build())
.textSizeDip(15)
.clickHandler(ParentComponentSendsEventToChild.onClickShowToast(c, childHandle)))
.child(
ChildComponentReceivesEventFromParent.create(c)
.textFromParent("Child with handle")
.handle(childHandle))
The parent uses the Handle reference to trigger an action on the child component:
@OnEvent(ClickEvent.class)
static void onClickShowToast(ComponentContext c, @Param Handle childHandle) {
ChildComponentReceivesEventFromParent.triggerOnShowToastEvent(
c, childHandle, "ChildComponent received event from parent!");
}
The action is defined on the child component using the @OnTrigger
annotation in Java:
@OnTrigger(ShowToastEvent.class)
static void triggerOnShowToastEvent(ComponentContext c, @FromTrigger String message) {
Toast.makeText(c.getAndroidContext(), message, Toast.LENGTH_SHORT).show();
}
Defining triggers in KComponents
is not supported yet, but they can invoke triggers as with Java Components.
Communicating between siblingsβ
Two sibling components (two child components of the same parent) cannot communicate directly. All communication must flow through the parent component, which intercepts events from a child component and notifies other child components of those events (using the methods detailed above).
A child component that needs to send a signal to a sibling component will dispatch an event to the common parent component:
@OnEvent(ClickEvent.class)
static void onSelectRadioButton(ComponentContext c, @Prop int id) {
ChildComponentSiblingCommunication.dispatchSelectedRadioButtonEvent(
ChildComponentSiblingCommunication.getSelectedRadioButtonEventHandler(c), id);
}
As shown in the following code, the parent component can:
- Perform a state update to recreate the sibling with new data (@OnUpdateState)
- Trigger an event on the sibling using a reference (@OnEvent).
@LayoutSpec
class ParentComponentMediatorSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext c, @State int selectedPosition) {
return Column.create(c)
.paddingDip(YogaEdge.ALL, 30)
.child(Text.create(c).text("ParentComponent").textSizeDip(30))
.child(
ChildComponentSiblingCommunication.create(c)
.id(0)
.isSelected(selectedPosition == 0)
.selectedRadioButtonEventHandler(
ParentComponentMediator.onSelectedRadioButtonEvent(c)))
.child(
ChildComponentSiblingCommunication.create(c)
.id(1)
.isSelected(selectedPosition == 1)
.selectedRadioButtonEventHandler(
ParentComponentMediator.onSelectedRadioButtonEvent(c)))
.build();
}
@OnEvent(SelectedRadioButtonEvent.class)
static void onSelectedRadioButtonEvent(ComponentContext c, @FromEvent int selectedId) {
ParentComponentMediator.onUpdateSelectedRadioButtonId(c, selectedId);
}
@OnUpdateState
static void onUpdateSelectedRadioButtonId(
StateValue<Integer> selectedPosition, @Param int selectedId) {
selectedPosition.set(selectedId);
}
}
Communicating externally to a componentβ
New data can be passed to a component from outside a Litho hierarchy by simply creating a new root component with new props.
There are multiple ways to perform an action on a component from outside a Litho hierarchy. The preferred method to pass new information to a component is by recreating it with new props; sometimes, it's necessary to trigger an action from non-Litho systems.
With an observerβ
An interface callback is invoked externally:
container.addView(
LithoView.create(
componentContext,
StateUpdateFromOutsideTreeWithListenerComponent.create(componentContext)
.eventObserver(observer1)
.build()));
final Button button1 = new Button(this);
button1.setText("Dispatch Event 1");
button1.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
observer1.notifyExternalEventOccurred();
}
});
The Component implements the callback and dispatches a state update on itself when the callback is invoked. No props or state should be captured in the callback: the callback will not be updated if they change, as illustrated in the following code.
@OnCreateInitialState
static void onCreateInitialState(
final ComponentContext c,
StateValue<Integer> counter,
@Prop ExternalEventObserver eventObserver) {
counter.set(0);
eventObserver.setListener(
new ExternalEventObserver.Listener() {
@Override
public void onMyEvent() {
// Note: you should not capture any props/state besides the ComponentContext here
// because they will not be updated for this callback if they change!
StateUpdateFromOutsideTreeWithListenerComponent.incrementCounter(c);
}
});
}
With a handleβ
A Handle reference can be created and passed to a Component, then used to invoke a trigger defined in the component:
final Handle componentHandle = new Handle();
final LithoView lithoViewWithTrigger =
LithoView.create(
componentContext,
StateUpdateFromOutsideTreeWithTriggerComponent.create(componentContext)
.handle(componentHandle)
.build());
final Button button2 = new Button(this);
button2.setText("Dispatch Event 2");
button2.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
StateUpdateFromOutsideTreeWithTriggerComponent.notifyExternalEvent(
// This is a bit of a gotcha right now: you need to use the ComponentContext from
// the ComponentTree to dispatch the trigger from outside a Component.
lithoViewWithTrigger.getComponentTree().getContext(),
componentHandle,
1 /* pass through the increment to show you can pass arbitrary data */);
}
});
Communicating externally from a Componentβ
To send events from a component to a listener outside of the Litho hierarchy, define an observer externally and invoke it from a component lifecycle method.
final ComponentContext c = new ComponentContext(this);
setContentView(
LithoView.create(
c,
ParentComponentReceivesEventFromChild.create(c)
.observer(
new ComponentEventObserver() {
@Override
public void onComponentClicked() {
Toast.makeText(
c.getAndroidContext(),
"Activity received event from child",
Toast.LENGTH_SHORT)
.show();
}
})
.build()));
}
public interface ComponentEventObserver {
void onComponentClicked();
}
Keep in mind that some lifecycle methods of Litho components can be invoked on background threads, so invoking callbacks from these methods might not be thread-safe if the callback produces side-effects.