Skip to main content

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.

class ParentComponentReceivesEventFromChildComponent(private val observer: ComponentEventObserver) :
KComponent() {

override fun ComponentScope.render(): Component {
val infoText = useState { "No event received from ChildComponent" }
return Column(style = Style.padding(all = 30.dp)) {
child(Text(text = "ParentComponent", textSize = 30.dp))
child(Text(text = infoText.value, textSize = 15.dp))
child(
ChildComponentSendsEventToParentComponent(
observer = observer, onChildClickEvent = { onNotifyParentEvent(infoText) }))
}
}

private fun onNotifyParentEvent(infoText: State<String>) {
infoText.update { "Received event from ChildComponent!" }
}
}

The child component can invoke the lambda 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.

class ChildComponentSendsEventToParentComponent(
private val observer: ComponentEventObserver,
private val onChildClickEvent: () -> Unit,
) : KComponent() {
override fun ComponentScope.render(): Component? {
return Column(style = Style.margin(all = 30.dp)) {
child(Text(text = "ChildComponent", textSize = 20f.sp))
child(
Text(
text = "Click to send event to parent!",
textSize = 15f.sp,
style =
Style.padding(all = 5.dp)
.border(
Border(
edgeAll = BorderEdge(color = Color.BLACK, width = 1.dp),
radius = BorderRadius(all = 2.dp)))
.onClick { onChildClickEvent() }))
child(
Text(
text = "Click to send event to Activity!",
textSize = 15f.sp,
style =
Style.padding(all = 5.dp)
.border(
Border(
edgeAll = BorderEdge(color = Color.BLACK, width = 1.dp),
radius = BorderRadius(all = 2.dp)))
.onClick { 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(
text = "Click to send new text to ChildComponent",
textSize = 15.dp,
style =
Style.padding(all = 5.dp)
.margin(top = 15.dp)
.border(
Border(
edgeAll = BorderEdge(color = Color.BLACK, width = 1.dp),
radius = BorderRadius(all = 2.dp)))
.onClick { version.update { it + 1 } }))
child(
ChildComponentReceivesEventFromParentComponent(
controller = controller, textFromParent = "Version ${version.value}"))

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 can interact with the child using controllers (see Controllers Pattern), which the parent creates and passes to the child component as a prop:

override fun ComponentScope.render(): Component? {

val controller = useCached { ParentToChildEventController() }
val version = useState { 0 }

return Column(style = Style.padding(all = 30.dp)) {
child(Text(text = "ParentComponent", textSize = 30.dp))
child(
Text(
text = "Click to trigger show toast event on ChildComponent",
textSize = 15.dp,
style =
Style.padding(all = 5.dp)
.margin(top = 15.dp)
.border(
Border(
edgeAll = BorderEdge(color = Color.BLACK, width = 1.dp),
radius = BorderRadius(all = 2.dp)))
.onClick { controller.trigger("Message from parent") }))
child(
ChildComponentReceivesEventFromParentComponent(
controller = controller, textFromParent = "Child with controller"))

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:

class ChildComponentSiblingCommunicationComponent(
private val id: Int,
private val isSelected: Boolean,
private val onSelected: (Int) -> Unit,
) : KComponent() {

override fun ComponentScope.render(): Component? {
return Row(style = Style.onClick { onSelected(id) }.margin(all = 30.dp)) {
child(
SolidColor(
color = if (isSelected) Color.BLUE else Color.WHITE,
style =
Style.width(20.dp)
.height(20.dp)
.margin(top = 10.dp, end = 30.dp)
.border(
Border(
edgeAll = BorderEdge(color = Color.BLUE, width = 1.dp),
radius = BorderRadius(all = 2.dp)))))
child(Text(text = "ChildComponent $id", textSize = 20.dp))
}
}
}

As shown in the following code, the parent component can:

  • Perform a state update to recreate the sibling with new data.
  • Trigger an event on the sibling using a reference.
class ParentComponentMediatorComponent : KComponent() {
override fun ComponentScope.render(): Component? {

val selectedPosition = useState { 0 }

return Column(style = Style.padding(all = 30.dp)) {
child(Text(text = "ChildComponent", textSize = 30.dp))
child(
ChildComponentSiblingCommunicationComponent(
id = 0,
isSelected = selectedPosition.value == 0,
onSelected = { selectedPosition.update(0) }))
child(
ChildComponentSiblingCommunicationComponent(
id = 1,
isSelected = selectedPosition.value == 1,
onSelected = { selectedPosition.update(1) }))
}
}
}

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,
new ParentComponentReceivesEventFromChildComponent(
() ->
Toast.makeText(
c.getAndroidContext(),
"Activity received event from child",
Toast.LENGTH_SHORT)
.show())));
}

public interface ComponentEventObserver {
void onComponentClicked();
}
tip

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.