To add a child RIB to another RIB
Building on top of what we achieved in tutorial1, we have our HelloWorld
RIB with its single button.
So far, we directly attached it to our integration point, RootActivity
, but now we want to see an example how to add it as a child RIB of another.
In the framework, individual RIBs are not forced to implement a view, they can also be purely business logic driven. In this case, they only add a Node
to the Node
tree, but no View
to the View tree.
Of course, they can do everything else, and can have child RIBs all the same. If these children have their own Views
, those will be attached directly to the View
of the closest parent up in the tree that has one.
Check the com.badoo.ribs.tutorials.tutorial2.rib
package in the tutorial2
module, you now see two RIBs: HelloWorld
, and GreetingsContainer
, the new one.
If you check RootActivity
, it builds and attaches GreetingsContainer
directly, and HelloWorld
is not used anywhere (yet).
The project should compile without any changes, and if you launch the app, this is what you should see:
Not too much to see here yet. GreetingsContainer
is a viewless container RIB, so it does not add anything to the screen. We will add functionality to it in later tutorials, right now the only thing we want to do is to attach HelloWorld
to it as its child.
To do that, first we need to familiarise ourselves with the concept of routing.
Parent - child relationships and their dynamic changes are described under the concept of routing.
In case of Badoo RIBs, routing is done via these steps:
- define the set of possible configurations a certain RIB can be in
- define what a certain configuration means, by resolving it to a routing action
- set the initial configuration for the Router
- (dynamically manipulate the Router to set it into any of the pre-defined configurations, based on business logic - in later tutorials)
A configuration is just a label used to describe the routing state.
For example, imagine a Root
RIB, which, on a very basic level, can have two possible subtrees attached: one if the user is logged out, and another if the user is logged in. But not both at the same time!
In this case, LoggedOut
and LoggedIn
would be the two possible configurations, which we could define as a Kotlin sealed class:
sealed class Configuration : Parcelable {
@Parcelize object LoggedOut : Configuration()
@Parcelize object LoggedIn : Configuration()
}
⚠️ Note how it implementsParcelable
, so that the framework can save/restore our configurations later.
Once we defined our possible configurations, we need to say what should happen when any of them is activated.
This is done by implementing the resolveConfiguration
method in the Router
:
abstract fun resolveConfiguration(configuration: C): RoutingAction
The library offers implementations of the RoutingAction
interface, which should cover all cases you encounter.
For simplicity, we will now only look at one of them: the AttachRibRoutingAction
, which, as the name implies, tells the Router
to attach a certain child RIB.
Let's have a look at routing for the GreetingsContainer
:
class GreetingsContainerRouter(
savedInstanceState: Bundle?
): Router</* ... */>(
savedInstanceState = savedInstanceState,
initialConfiguration = Configuration.Default
) {
sealed class Configuration : Parcelable {
@Parcelize object Default : Configuration()
}
override fun resolve(routing: RoutingElement<Configuration>): RoutingAction =
RoutingAction.noop()
}
This looks pretty basic:
- The possible set of configurations contain only one:
Default
GreetingsContainerRouter
passes inConfiguration.Default
asinitialConfiguration
in the parent constructor. AllRouters
need to state their initial configurations.- When resolving configurations, we always return
noop()
as aRoutingAction
, doing nothing
Let's spice it up a bit!
First, rename Default
to describe what we actually want to do:
sealed class Configuration : Parcelable {
@Parcelize object HelloWorld : Configuration()
}
Next, change the resolution so that it attaches something:
import com.badoo.ribs.routing.action.AttachRibRoutingAction.Companion.attach
/* ... */
override fun resolve(routing: RoutingElement<Configuration>): RoutingAction =
when (routing.configuration) {
is Configuration.HelloWorld -> attach { TODO() }
}
⚠️ Notice howAttachRibRoutingAction
also offers a convenience methodattach
to construct it, for which you can use static imports. The same goes for otherRoutingActions
, too.
⚠️ Also notice that even thoughConfiguration.HelloWorld
is a Kotlin object, there's still anis
for it in thewhen
expression. This is actually important, since after save/restore cycle, the instance restored fromBundle
will be a different instance! Without usingis
, thewhen
expression will not match in that case.
Alright, but what to put in place of the TODO()
statement?
Looking at the signature of the attach
method it needs a lambda that can build another Node
given a nullable Bundle
(representing savedInstanceState
):
fun attach(builder: (Bundle?) -> Node<*>): RoutingAction
To build the HelloWorld
RIB, we need an instance its Builder: HelloWorldBuilder
, just as we did in tutorial1. So first just add it as a constructor dependency to our Router:
class GreetingsContainerRouter(
private val helloWorldBuilder: HelloWorldBuilder
) /* rest is the same */
We'll care about how to pass it here just in a moment, but first let's finish our job here, and replace the TODO()
block in our attach
block:
override fun resolve(routing: RoutingElement<Configuration>): RoutingAction =
when (routing.configuration) {
is Configuration.HelloWorld -> attach { helloWorldBuilder.build(it) }
}
Our job is done here in the GreetingsContainerRouter
, let's see how we can provide the Builder we need.
Badoo RIBs relies on Dagger2 to provide dependencies. In the builder
subpackage, let's open GreetingsContainerModule
, and notice how the first method constructs the Router:
@GreetingsContainerScope
@Provides
@JvmStatic
internal fun router(
// pass component to child rib builders, or remove if there are none
component: GreetingsContainerComponent
): GreetingsContainerRouter =
GreetingsContainerRouter()
Notice how the constructor invocation has a compilation error now, since we don't yet pass in the required dependency. Let's do that, and change the constructor invocation to:
GreetingsContainerRouter(
helloWorldBuilder = HelloWorldBuilder()
)
Now it's the HelloWorldBuilder()
part that's missing something. If we open it up, we get a reminder that in order to construct it, we need to satisfy the HelloWorld
RIB's dependencies.
Notice how the function has an unused parameter we can use:
internal fun router(
// pass component to child rib builders, or remove if there are none
component: GreetingsContainerComponent
)
Let's do that!
GreetingsContainerRouter(
helloWorldBuilder = HelloWorldBuilder(component)
)
Notice the compilation error again:
That's fine and expected! We need to make the connection and say that HelloWorldComponent
should actually provide those dependencies:
For simplicity, we removed all actual dependencies from
HelloWorld.Dependency
interface, so that we can attach it as easily as possible.Don't worry, we will add it back in the next tutorial!
The application should now compile, and launching it, this is what we should see:
It's not very different from what we've seen in tutorial1, but now it's added as a child RIB of GreetingsContainer
.
Note that because we removed the
Consumer<Output>
dependency fromHelloWorld
, the button now doesn't do anything when pressed. We'll fix that soon!
You can advance to the next tutorial!
Steps to add a child RIB:
- Go to Router
- Define configuration
- Resolve configuration to
attach { childBuilder.build(it) }
routing action - Define
childBuilder
as a constructor dependency
- Go to Dagger module
- When constructing the
Router
, create and pass in an instance of the required childBuilder
- Add
component
as a parameter toBuilder
constructor - Let our component extend the required
Dependency
interface - (Actually satisfy those dependencies - coming up in next tutorial)
- When constructing the