Dagger Injection in an Android Library
Recently, we came across a case where one feature had to be used within multiple android apps. These apps don’t share their codebase, hence we decided to create an Android Library (aar).
Our library will consist of some UI Components, business logic and few dependencies from the host app with activity as the entry point. All the host apps were using Dagger-2 as their dependency injections framework, so we decided to use the same since it will reduce the learning curve.
First I would like to talk about our initial approach where the library’s object graph was merged with the host apps object graph.
We started by creating an activity, setting up clean architecture and used Dagger-2 to satisfy all internal dependencies. Then we define a LibraryModule
which will be added to the host app’s AppComponent
to satisfy all dependencies.
@Module(includes = [SomeModule::class, ...])
class LibraryModule {
//Provides objects to satisfy internal dependencies
}@Component(module = [...,
LibraryModule::class
])
interface AppComponent {
fun inject(application: MyApplication)
...
}
With the above implementation, the library’s object graph is merged with the host app’s object graph. So all library required dependencies (for example. an OkHttpClient
instance) from the host app will be easily satisfied.
Then we will provide an interface to the host app which will tell the library to start the activity.
interface LibraryClient {
fun startActivity(context: Context)
}
That’s it! Our library worked fine with the host app.
But something doesn’t seem right about this approach, the host app knows a lot about the library and we clearly don’t want that. We want the host to know only about the exposed contract nothing else, which means we will have to reject this approach.
Now how to resolve the above issue? How to keep the library’s object graph detached from the host app while still using Dagger. The only way to resolve this is by not providing LibraryModule
to the host app.
So we went on a different route.
We wanted to have dagger injections in our Android library and also don’t want the host app to append the library’s object graph into its own. Dagger has the ability to initialize the object graph from Application, Activity or a Fragment. We decided to go with activity, since the library only has one activity which is an entry point and multiple fragments within itself, so it seemed like a good bet.
Let’s create a dagger module to satisfy internal dependencies.
@Module(includes = [SomeModule::class, ...])
internal class LibraryModule {
//Provides objects to satisfy internal dependencies
}
Now lets create the component which can be injected from our Activity.
@Component(modules = [LibraryModule::class])
internal interface LibraryComponent {
fun inject(activity: MainActivity)
}internal class MainActivity : AppCompatActivity() {
@Inject
lateinit var presenter: MainPresenter
override fun onCreate(savedInstanceState: Bundle?) {
DaggerLibraryComponent()
.create()
.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//some presenter actions
}
}
To make the injection more easy to understand let’s create a static method.
internal class MainScreenInjector {
companion object {
fun inject(activity: MainActivity) {
DaggerLibraryComponent()
.create()
.inject(activity)
}
}
}internal class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
MainScreenInjector.inject(this)
...
}
}
Next, we will expose an interface LibraryClient
for the host app.
interface LibraryClient {
fun startActivity(context: Context) companion object {
fun getInstance(): LibraryClient {
return LibraryClientImpl()
}
}
} internal class LibraryClientImpl : LibraryClient {
override fun startActivity(context: Context) {
context.startActivity(Intent(context, MainActivity::class.java)
}
}
A quick note — we used internal
so that all those classes are private to the library and hidden from the outside world. Now, the host app will be able to use the Library with a simple implementation.
class MyActivity : AppCompatActivity() {
... fun someMethod() {
val libraryClient = LibraryClient.getInstance()
libraryClient.startActivity(this)
}
...
}
This seems like the right approach, the host does not know how the library works or what the library’s object graph looks like. The host only has a contract which can be used when it wants to interact with the library. Kudos!!
Now comes the part where the library has a dependency on the host app for OkHttpClient. To satisfy this dependency host has to somehow provide the OkHttpClient while requesting an instance of our LibraryClient
, something like this.
LibraryClient.getInstance(okHttpClient)
But, if OkHttpClient is passed as an argument then the library will have difficulty in providing it internally since our library relies on Dagger to satisfy the dependencies. So let’s find out how we can use Dagger to satisfy our OkHttpClient requirement. We will start by modifying our LibraryClient
to expect a dependency from the host.
interface LibraryClient {
fun startActivity(context: Context) companion object {
fun getInstance(deps: Dependency): LibraryClient {
return LibraryClientImpl()
}
}
interface Dependency {
fun getOkHttpClient(): OkHttpClient
}
}
Now, the host will have to implement the LibraryClient.Dependency
in order to utilise the library.
val libraryClient = LibraryClient.getInstance(
object : LibraryClient.Dependency {
override fun getOkHttpClient() {
return OkHttpClient.Builder()
.build()
}
}
}
libraryClient.startActivity(this)
We still have to provide this dependency via Dagger to our library, to do this lets take a look at our Component
again.
Dagger’s component accepts two parameters
modules
anddependencies
. Which means that a component can expect some dependencies from outside.
So, let’s make LibraryClient.Dependency
as our component dependency.
@Component(modules = [LibraryModule::class],
dependencies = [LibraryClient.Dependency::class])
internal interface LibraryComponent {
fun inject(activity: MainActivity)
}
Since the component now has a dependency, we will have to modify our MainScreenInjector
to provide these dependencies as well.
internal class MainScreenInjector {
companion object {
private var deps: LibraryClient.Dependency? = null
fun inject(activity: MainActivity) {
DaggerLibraryComponent()
.builder()
.dependency(deps)
.build()
.inject(activity)
}
fun setDependency(dependency: AuthClient.Dependency) {
this.deps = dependency
}
}
}
Now our LibraryClient
can just set the dependency in our screen injector to be utilised by our Component
during injection.
interface LibraryClient {
...
fun getInstance(deps: Dependency): LibraryClient {
MainScreenInjector.setDependency(deps)
return LibraryClientImpl()
} interface Dependency {
fun getOkHttpClient(): OkHttpClient
}
}
And Voila! It’s done. The host is only aware of the contract our library has exposed and a list of dependencies which it will have to provide while requesting for an instance. The dependencies are passed on to our component through the MainScreenInjector
. With this implementation, we don’t even care if the host is using dagger or not, since the library has its own DI layer inside.
PS: For fragments in our library we can use Subcomponent
pattern with our LibraryComponent
as its base component (Refer Injecting Fragment Objects), which can be used to install modules for our fragments.
This is how we were able to use Dagger-2 within our library and retain the separation of concern between the library and the host(s). Comment below and let us know what approach you took for using DI within a library.