The framework includes the ChildAware
interface, which comes with a powerful API.
It allows you to scope communication with (or between) dynamically available children easily.
We can assume the following for the next examples:
- Let's assume that
SomeInteractor
belongs toSomeNode
(and is passed as a Plugin to it) - Let's assume
SomeNode
can host multiple children:Child1
,Child2
, etc. - We know that the
Interactor
base class in the framework implementsNodeLifecycleAware
, which makes sure we will receive theonCreate
callback from the framework - We know that
Interactor
implementsViewAware
, which means we will receive theonViewCreated
callback too Interactor
also implementsChildAware
, which unlocks thechildAware
DSL we'll see in the following snippets
import androidx.lifecycle.Lifecycle
import com.badoo.ribs.clienthelper.childaware.childAware
import com.badoo.ribs.clienthelper.interactor.Interactor
import com.badoo.ribs.core.modality.BuildParams
class SomeInteractor(
buildParams: BuildParams<*>,
// ...
) : Interactor<SomeRib, SomeRibView>(
buildParams = buildParams
) {
override fun onCreate(nodeLifecycle: Lifecycle) {
super.onCreate(nodeLifecycle)
childAware(nodeLifecycle) {
whenChildAttached<Child1> { commonLifecycle, child1 ->
// TODO e.g.
// - subscribe to child1.output
// - feed something to child1.input
// - use commonLifecycle for scoping reactive subscriptions
}
}
}
}
Let's unpack what's happening here:
- We pass
SomeNode
'snodeLifecycle
as the upper bound for lifecycle scoping - Using
whenChildAttached<Child1>
we tell the framework to execute the subsequent lambda whenever any child implementing theChild1
interface is attached. - In the lambda, we receive
commonLifecycle
: This is the minimum common lifecycle of the parent and the child, and is safe to scope any subscriptions by. - Also in the lambda, we receive an instance of the attached child, here
child1
- We can then use these to communicate with
child1
for the duration it is attached to the parent - When scoped properly, all communication will be disposed of when the child is detached
- The lambda will be invoked again automatically for all subsequent attaches of
Child1
class SomeInteractor(
buildParams: BuildParams<*>,
// ...
) : Interactor<SomeRib, SomeRibView>(
buildParams = buildParams
) {
override fun onViewCreated(view: SomeView, viewLifecycle: Lifecycle) {
childAware(viewLifecycle) {
whenChildAttached<Child1> { commonLifecycle, child1 ->
// TODO e.g.
// - connect view to child1.input
// - connect child1.output to view
// - use commonLifecycle for scoping reactive subscriptions
}
}
}
}
In this snippet:
- We pass the parent's
viewLifecycle
ofSomeNode
as the upper bound for lifecycle scoping - This means that the
commonLifecycle
we receive in the lambda will be the minimum of:- The view's lifecycle: if the view is destroyed, this lifecycle will move to a
DESTROYED
state too - The lifecycle of
child1
: if the child is detached, this lifecycle will move to aDESTROYED
state too
- The view's lifecycle: if the view is destroyed, this lifecycle will move to a
- (The above of course applies for start / stop / resume / pause lifecycle events and their corresponding states too!)
class SomeInteractor(
buildParams: BuildParams<*>,
// ...
) : Interactor<SomeRib, SomeRibView>(
buildParams = buildParams
) {
override fun onCreate(nodeLifecycle: Lifecycle) {
super.onCreate(nodeLifecycle)
childAware(nodeLifecycle) {
whenChildrenAttached<Child1, Child2> { commonLifecycle, child1, child2 ->
// TODO e.g.
// - subscribe child1.output to child2.input with some mapping
// - use commonLifecycle for scoping reactive subscriptions
}
}
}
}
In this snippet:
- We use
whenChildrenAttached
instead ofwhenChildAttached
: this variant connects multiple children - The framework will invoke our lambda every time two children of the given types are both attached to the parent
- The
commonLifecycle
we receive in the lambda will now be the minimum of:- The parent's lifecycle
- The lifecycle of
child1
- The lifecycle of
child2
- Any of them e.g. is paused, the common lifecycle will also move back to the corresponding
STARTED
state - Any of them e.g. is destroyed, the common lifecycle will also move to a
DESTROYED
state
You can,of course use this api scoped for the view lifecycle too similarly to the previous example.
MVICore has some powerful tools like the Binder
which gives you automatically managed, scoped reactive bindings.
To leverage them, you'll need to add dependency on:
MVICore / Binder
implementation 'com.github.badoo.mvicore:binder:{latest-mvicore-version}'
The :rib-mvicore
artifact from this library:
implementation 'com.github.badoo.RIBs:rib-mvicore:{latest-RIBs-version}'
Then you can leverage the full power of their combined API:
import androidx.lifecycle.Lifecycle
import com.badoo.binder.using
import com.badoo.ribs.clienthelper.childaware.childAware
import com.badoo.ribs.clienthelper.interactor.Interactor
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.mvicore.createDestroy
import com.badoo.ribs.mvicore.resumePause
import com.badoo.ribs.mvicore.startStop
class SomeInteractor(
buildParams: BuildParams<*>,
// ...
) : Interactor<SomeRib, SomeRibView>(
buildParams = buildParams
) {
override fun onCreate(nodeLifecycle: Lifecycle) {
super.onCreate(nodeLifecycle)
childAware(nodeLifecycle) {
createDestroy<Child1> { child1 ->
bind(child1.output to someConsumer)
bind(someSource to child1.input)
}
startStop<Child1, Child2> { child1, child2 ->
bind(child1.output to child2.input using SomeMapper)
}
resumePause<Child1, Child2> { child1, child2 ->
bind(child2.output to child1.input using SomeOtherMapper)
}
}
}
}
Let's unpack what's going on here:
-
Binder
from MVICore gives us thebind(A to B)
syntax- This creates a reactive subscription between A and B
- This subscription is always scoped by a lifecycle
- When that lifecycle is destroyed, so is the subscription
- But should that lifecycle start again,
Binder
will automatically re-subscribe A to B
-
The keywords you see:
createDestroy
scopes a connection between create / destroy events of the lifecyclestartStop
scopes a connection between start / stop events of the lifecycleresumePause
scopes a connection between resume / pause events of the lifecycle
-
Also note how the
commonLifecycle
is no longer used explicitly, it's handled automatically under the hood -
Following from this, the last example in the snippet will:
- Create a subscriptions between
child2.output
andchild1.input
using a mapper to convert objects - Automatically dispose of the subscription whenever either the parent, or
child1
, orchild2
gets paused - Automatically re-subscribes again whenever all of them are in a resumed state again
- Points 2-3 can be repeated indefinitely
- Create a subscriptions between