Understanding how to dynamically switch between child RIBs
Launch the tutorial5 app, and you can see that there's a new button in the layout: MORE OPTIONS.
If you look at the project structure, you can also find a new RIB in this module: OptionSelector.
All it does is it renders a screen with text options and a confirm button. We will use that to update the button text in our HelloWorld rib, and also use it for the actual greeting shown in the Snackbar.
We had this hiearachy so far:
GreetingsContainer
└── HelloWorld
And now we'll add OptionsSelector as the second child of GreetingsContainer:
GreetingsContainer
├── HelloWorld
└── OptionsSelector
The idea how they will work together:
- On
HelloWorldscreen, User presses MORE OPTIONS. Since it is beyond the responsibility ofHelloWorldRIB, it reports it asOutput GreetingsContainercatches the output, and switches its routing fromHelloWorldtoOptionsSelector. Since we display the container on the whole screen, this results in a "new screen" effect.OptionsSelectoroffers UI interaction to select something from a radio group. What should happen when a certain options is selected is beyond its responsibilities, so similarly as withHelloWorld, it reports it asOutput.GreetingsContainercatches the output, switches back its routing toHelloWorldagain.- The text of the main button in
HelloWorldshould be updated to reflect the newly selected option. This can be done by via anInputcommand toHelloWorldwhich allows setting of the text from outside of the RIB.
By now you should be able to:
- Trigger a new event from the UI that reaches the parent as
Output- Add a new element to
OutputinHelloWorldcalledShowMoreOptions - Add a new element to
EventinHelloWorldViewcalledMoreOptionsButtonClicked - In
HelloWorldView, set a click listener onmoreOptionsButtonthat will publishMoreOptionsButtonClicked - Add the transformation between
EventandOutputin theviewEventToOutputtransformer - React to this new output in
GreetingsContainerInteractor. Leave the actual implementation aTODO()
- Add a new element to
- Add
OptionSelectorRIB as a child ofGreetingsContainer. This involves:- making
GreetingsContainerComponentextend child dependency interface - satisfying child dependencies (prepared for you in
GreetingsContainerModule) - providing
optionsSelectorBuilderto theGreetingsContainerRouter - adding a new Configuration to
GreetingsContainerRouter: "OptionsSelector" - resolving it to an
attach { optionsSelectorBuilder.build() }action
- making
For help with the above tasks, you can refer to:
- tutorial1 / Further reading section on how to make a Button trigger an
Output - tutorial2 / Summary section on how to add a child RIB to a parent
- tutorial4 on commnunication with child RIBs, i.e.
Inputs/Outputs
Right now:
- our new Button can signal the correct
Output - the container's
Routercan build the other RIB we need
The only thing we need is to connect the dots, so that 1. actually triggers doing 2.
Business logic triggers routing:
- in
GreetingsContainerInteractorwe consume theOutputofHelloWorld - in the
whenbranch for the newOutput(where we added aTODO()) we want to tellGreetingsContainerRouterto switch to the Configuration representingOptionSelectorRIB.
All we need to do is:
class GreetingsContainerInteractor
// ...
internal val helloWorldOutputConsumer: Consumer<HelloWorld.Output> = Consumer {
when (it) {
HelloThere -> output.accept(GreetingsSaid("Someone said hello"))
ShowMoreOptions -> router.push(Configuration.OptionsSelector)
}
}
}Pressing the MORE OPTIONS button the app should display the new screen:
Try it!
Right now the only way of getting back to HelloWorld is to press back on the device. We'll address that soon.
Why did the above work?
All Routers have a routing back stack. By default, this back stack has a single element:
back stack = [(initial configuration)]
This is the one you set in your Router. In GreetingsContainerRouter this reads:
class GreetingsContainerRouter(
// ...
initialConfiguration = Configuration.HelloWorld
)So our default back stack was in fact:
back stack = [*Configuration.HelloWorld]
A configuration can be either active/inactive. In simple terms, it's active if it's on screen.
A simplified rule of the back stack is that only the last configuration is active. We'll mark this with an asterisk (*) from now on.
Router offers you operations to manipulate this back stack.
fun push(configuration: Content)
fun popBackStack(): BooleanThere are other operations too, but we'll discuss them later in other tutorials. What's important is that push adds a new element to the end of the back stack, while popBackStack removes the last one from the end.
So when we did router.push(Configuration.OptionsSelector), this happened:
back stack 0 = [*Configuration.HelloWorld]
// push
back stack 1 = [Configuration.HelloWorld, *Configuration.OptionsSelector]
And because we just said that the last element in the back stack is active (on screen), this means that the view of HelloWorld gets detached, and OptionsSelector gets created and attached.
The reverse is happening when we pressed back.
By default, back pressing is propagated to the deepest active levels of the RIB tree by default, where each RIB has a chance to react to it:
Interactorhas a chance to overridehandleBackPress()to do something based on business logicRouterwill be asked if it has back stack to pop. If yes, pop is triggered and nothing else is done.- If
Routerhad only one more configuration left in its back stack, there's nothing more to remove. The whole thing starts bubbling up the RIB tree to higher levels until one of the levels can handle it (points 1 and 2). If it is handled, the propagation stops. - If the whole RIB tree didn't handle the back press, then the last fallback is the Android environment (in practice this probably means that the hosting
Activityfinishes).
In our case, when we were on the OptionsSelector screen, GreetingsContainerRouter had 2 elements in its back stack, so it could automatically handle the back press event by popping the latest:
back stack 0 = [*Configuration.HelloWorld]
// push
back stack 1 = [Configuration.HelloWorld, *Configuration.OptionsSelector]
// back press
back stack 2 = [*Configuration.HelloWorld]
And again, because last element in the back stack is on screen, this means that OptionsSelector gets detached, and HelloWorld gets attached back to the screen again.
Of course there's no point of opening the second screen if we cannot interact with it and our only option is to press back.
So let's make it a bit more useful:
- Add a new element to
OutputinOptionsSelector:data class OptionSelected(val text: Text) : Output() - Add a new element to
EventinOptionsSelectorView:data class ConfirmButtonClicked(val selectionIndex: Int) : Event() - In
OptionsSelectorViewImpl, add a click listener onconfirmButtonthat will trigger this event. - Go to
OptionSelectorInteractor. Add the transformation betweenEventandOutputin theviewEventToOutputtransformer. - Go to
GreetingsContainerInteractor, and add a branch to thewhenexpression inmoreOptionsOutputConsumer
What we want to do is:
- Take the
Textthat's coming in theOutput - Feed it to
HelloWorldusing an Input ofUpdateButtonText - Actually go back one screen = manually popping the back stack
This is how it might look:
internal val optionsSelectorOutputConsumer: Consumer<OptionSelector.Output> = Consumer {
when (it) {
is Output.OptionSelected -> {
router.popBackStack()
helloWorldInputSource.accept(
UpdateButtonText(it.text)
)
}
}
}At this point we should be able to go to options selection screen, chose an item from the radio group, and pressing the confirm button we should land back at the Hello world! screen with the label of the hello button reflecting our choice.
Press the button!
We just created more complex functionality by a composition of individually simple pieces!
When we created our hierarchy like this, we kept the two children decoupled from each other:
GreetingsContainer
├── HelloWorld
└── OptionsSelector
Even though they work together as a system, HelloWorld and OptionsSelector has no dependency on each other at all.
This is actually beneficial, because:
OptionsSelectoris a generic screen (it renders whatever text options we build it with)- From the perspective of
HelloWorldit really shouldn't care where it gets its other greetings
Keeping them decoupled means:
OptionsSelectorcan be reused freely elsewhere to render the same screen with other optionsHelloWorldcan be reused with different implementation details how to provide more options to it
The combined functionality we just implemented emerges out of the composition of these pieces inside GreetingsContainer. Each level handles only its immediate concerns:
HelloWorldimplements hello functionality and can ask for more optionsOptionsSelectorrenders options and signals selectionGreetingsContainerconnects the dots and contains only coordination logic. All other things are delegated to child screens as implementation details.
Congratulations! You can advance to the next one.
Dynamic routing
- Make the parent RIB be able to build a child RIB (as seen in tutorial2):
- Add configuration & routing action in Router
- Provide child
Buildervia DI
- React to some event (usually to child
Outputas seen in tutorial4, but can be anything else) inInteractorof parent RIB by pushing new configuration to itsRouter - Use back press or
popBackStack()programmatically to go back
Composing functionality
- Instead of one messy RIB, map complex functionality to a composition of simple, single responsibility RIBs
- When composing, keep parent levels simple. They should only coordinate between child RIBs by
Inputs/Outputs, and delegate actual functionality to children as implementation details. - Sibling RIBs on the same level should not depend on each other, so that they can be easily reused elsewhere.




