To see how to provide dependencies to a RIB
Compiling and launching tutorial3, this is what we see:
We will provide text to the placeholders first manually, then as a dependency.
Have a brief look at the Text
interface found in the library, as we will use it in the example:
package com.badoo.ribs.android
import android.content.Context
/**
* An abstraction over text, so that you can provide it from different sources.
*
* In case the default implementations are not good enough, feel free to implement your own.
*/
interface Text {
fun resolve(context: Context): String
class Plain(private val string: String): Text {
override fun resolve(context: Context): String =
string
}
class Resource(private val resId: Int, private vararg val formatArgs: Any) : Text {
override fun resolve(context: Context): String =
context.resources.getString(resId, formatArgs)
}
}
It provides us with an abstraction over textual information, and two simple implementations – one for actual Strings, the other for String resources.
This is a useful approach for cases when you want to set a text from resource, but at the place of definition you don't have access to a Context
that Android can provide, only later. Hence the resolve(context: Context)
method.
Let's start with the second placeholder first!
Check the HelloWorldView
interface. Its ViewModel
contains a dummy integer field only:
data class ViewModel(
val i: Int = 0
)
Let's change that so that we will give a Text
to the view to render instead:
data class ViewModel(
val welcomeText: Text
)
Let's scroll down to the Android view implementation. Notice how it already finds a reference to the welcome text view:
private val welcome: TextView = androidView.findViewById(R.id.hello_world_welcome)
So let's implement the ViewModel
rendering:
override fun accept(vm: ViewModel) {
welcome.text = vm.welcomeText.resolve(androidView.context)
}
Notice how we added welcomeText
as a Text
, which we could now resolve using the Context
our view has access to.
Let's head to our Interactor in this RIB, HelloWorldInteractor
, where we should put business logic.
We'll find an empty block, where we can put all view-related business logic, tied to the lifecycle of the view:
override fun onViewCreated(view: HelloWorldView, viewLifecycle: Lifecycle) {
super.onViewCreated(view, viewLifecycle)
}
Just to try out what we did, we could do something like this to test out the parts we just implemented in HelloWorldView
:
override fun onViewCreated(view: HelloWorldView, viewLifecycle: Lifecycle) {
super.onViewCreated(view, viewLifecycle)
view.accept(initialViewModel)
}
private val initialViewModel =
HelloWorldView.ViewModel(
welcomeText = Text.Plain("Does this work at all?")
)
Launch the app to verify that it indeed works.
Alright, now instead of manually fixing this, let's try and make this a dependency!
Let's head to HelloWorld
, the main interface of the RIB, which doesn't have any dependencies now:
interface HelloWorld : Rib {
interface Dependency
// ...
}
Change it so that it looks like:
interface HelloWorld : Rib {
interface Dependency {
fun config(): Config
}
// It's a good idea to group all "simple data" dependencies into a Config
// object, instead of directly adding them to Dependency interface:
data class Config(
val welcomeMessage: Text
)
// ...
}
Now if we try to build the project, Dagger will fail us, as we do not yet actually provide this dependency:
[Dagger/MissingBinding] com.badoo.ribs.tutorials.tutorial3.rib.hello_world.HelloWorld.Config cannot be provided without an @Inject constructor or an @Provides-annotated method.
public abstract interface GreetingsContainerComponent extends com.badoo.ribs.tutorials.tutorial3.rib.hello_world.HelloWorld.Dependency
In this case, our dependency is simple configuration data, so we could just satisfy it directly in the parent.
Let's head to GreetingsContainerModule
, the place where we add all the @Provides
DI definitions on the container level, and add this block to the bottom:
@GreetingsContainerScope
@Provides
@JvmStatic
internal fun helloWorldConfig(): HelloWorld.Config =
HelloWorld.Config(
welcomeMessage = Text.Resource(
R.string.hello_world_welcome_text
)
)
Now the app should build, as we provide the dependency - other than that, nothing changed, since we are not using this config yet.
Let's correct that, and make use of it in the child Interactor
.
Add the config to the constructor, and use it to construct the initial ViewModel
:
class HelloWorldInteractor(
config: HelloWorld.Config, // add this
router: // ... remainder omitted
) {
override fun onViewCreated(view: HelloWorldView, viewLifecycle: Lifecycle) {
super.onViewCreated(view, viewLifecycle)
view.accept(initialViewModel)
}
private val initialViewModel =
HelloWorldView.ViewModel(
welcomeText = config.welcomeMessage // use it
)
}
And change the actual construction of the Interactor in HelloWorldModule
:
@HelloWorldScope
@Provides
@JvmStatic
internal fun interactor(
config: HelloWorld.Config, // add this, Dagger will provide it automatically
router: HelloWorldRouter
): HelloWorldInteractor =
HelloWorldInteractor(
config = config, // pass it to the Interactor
router = router
)
Try it out, it should now display the text we passed in as a resource:
- Add new widget to xml
- Find view by id and store reference in
ViewImpl
- Define new field in
ViewModel
- Modify
accept(vm: ViewModel)
method inViewImpl
to actually display data - In case of initial
ViewModel
: pass it fromInteractor
'sonViewCreated
method (we'll cover dynamically changing data later)
- Add it to child's
Dependency
interface - In parent's
Module
class, add new@Provides
annotated block - Use dependency in child where it's needed
- e.g. add it to constructor of
Interactor
- go to child's
Module
/ provides function - add it as a parameter to
@Provides
function - this will be provided by Dagger - use it in constructing the object, e.g. the
Interactor
- e.g. add it to constructor of
We are still left with a placeholder text to be filled with text.
Let's imagine this application has some kind of User
object representing the current logged in user, and we want this placeholder to greet the user like: "Hello User!", with their actual name.
(There's a User
interface included in this module, check it out)
Obviously, the HelloWorld
RIB cannot fill in the name of the user, and will need an instance of User
as a dependency.
In the case of the welcome text, we could provide the dependency directly. Problem is, finding an instance of User
is not the responsibility of HelloWorld
, but it's also not the responsibility of the parent RIB, the GreetingsContainer
.
So in this case, we will need to bubble up this dependency until at some level we can grab an instance of the current User
, and pass it down.
(As we do not have proper logged out / logged in handling yet, we wil just pass in a dummy User object from the Activity level)
Based on the previous sections, you should be able to:
- Add a new field to
HelloWorldViewImpl
that holds a reference to theTextView
for the other placeholder, found inrib_hello_world.xml
with the id@+id/hello_world_title
- Add a new field to
HelloWorldView.ViewModel
, namedtitleText
, typeText
- Set the text of the
TextView
by resolving theText
fromtitleText
whenever theViewModel
is rendered inHelloWorldViewImpl
- Set a fixed value for this field from
HelloWorldInteractor
just to test it out.
Now let's make it more dynamic. You should also be able to:
- Add an instance of
user: User
as a constructor dependency toHelloWorldInteractor
- Use
user.name()
to construct:Text.Resource(R.string.hello_world_title, user.name())
, and pass it as the value fortitleText
when creating theViewModel
- Add the instance of
User
as a dependency for the creation ofHelloWorldInteractor
in the respective@Provides
function inHelloWorldModule
- Add
User
toHelloWorld.Dependency
interface to say thatHelloWorld
RIB needs this from the outside
If you feel stuck, you can refer to the previous sections for help, or have a peek at solutions.
Building the project at this point, Dagger should give you:
Dagger/MissingBinding] com.badoo.ribs.tutorials.tutorial3.util.User cannot be provided without an @Provides-annotated method.
This will be super easy, if you got this far. The only difference is that if we cannot provide a dependency directly, we also add it to the Dependency
interface of parent. We can keep doing this further until on some level we can actually provide it.
Really, just a one-liner.
Try it:
interface GreetingsContainer : Rib {
interface Dependency {
// Add this, and you are done on this level - Dagger will provide
// it further down to children automatically:
fun user(): User
fun greetingsContainerOutput(): Consumer<Output>
}
// ... remainder omitted
}
At this point we reached the root level. RootActivity
creates an anonymous object actually providing dependencies to GreetingsContainer
, so we have a compilation error there until we actually implement the newly added user()
method:
/** The tutorial app's single activity */
class RootActivity : RibActivity() {
// ... remainder omitted
override fun createRib(savedInstanceState: Bundle?): Node<*> =
GreetingsContainerBuilder(
object : GreetingsContainer.Dependency {
// add this block:
override fun user(): User =
User.DUMMY
override fun greetingsContainerOutput(): Consumer<GreetingsContainer.Output> =
// ... remainder omitted
}
).build(savedInstanceState)
}
The application should now build, and this is what you should see when launching:
Congratulations! You can advance to the next one.