If you aren't familiar with Inversion of Control or Why To Care about Dagger, you may want to start there first.
I'll be explaining:
- Concepts you'll need specific to dagger-android
- Why we're using dagger android despite it being officially deprecated and why I think it's still the correct choice for the next several months at least.
- Imports.
- Initial Setup.
- Expanding onto new features.
- Next Steps
- Drawbacks
Jump to whatever section you like. If any part isn't clear, leave a comment and I'll make it more understandable!
Concepts
Dagger lets you easily handle Inversion of Control and get down to just writing the code for the features you wanted to add!
So let's say you have the following.
- An Activity which shows some list.
- A presenter, which calculates the list.
- A network source which downloads raw data for the list.
With manual Dependency Injection you may set up the activity like so:
class MainActivity : AppCompatActivity() {
lateinit var presenter : Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter = CitiesPresenter(CitiesRepository(Retrofit()))
}
}
Whew that's a lot and we didn't even pass in a view interface yet!
Note: if this part is unclear, please refer to the earlier article on Inversion of Control.
When you've got Dagger-Android set up here, the same code (with a bit more classes to help out that I haven't mentioned yet) can look like this.
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var presenter : Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
And whenever you change how the presenter is constructed, it'll just change all the boilerplate of presenter = CitiesPresenter(CitiesRepository(Retrofit()))
right along with it! Without you having to do anything else.
So let's see how we can get started on this conversion.
Why we're using dagger android
A quick note on why we're using dagger-android.
At Android Dev Summit 2019, it was announced that Dagger-Android (the extension library to dagger, not dagger itself) is now deprecated.
They won't be working on any new features for it and are still working on the replacement for it which will have an even easier API.
So why am I writing about how to use Dagger-Android? It's because they're still working on a replacement.
In my opinion, nothing is deprecated until there's a replacement. You may decide fossil fuel based cars are deprecated, and we should all move off it. But no one's going back to walking 10km to work. Until the electric bus, or public transport or something takes its place. People are still going to keep using it and reasonably so.
If you really want an alternative that's better, albeit a bit more technically tricky, then listen to this talk.
If you want to keep using Dagger, and do DI the right way until an easier alternative comes out, keep reading!
Imports
We're first going to need to add the library and compiler/annotation-processor for both dagger and the dagger-android support classes.
implementation com.google.dagger:dagger:2.24
kapt com.google.dagger:dagger-compiler:2.24
implementation com.google.dagger:dagger-android-support:2.24
kapt com.google.dagger:dagger-android-processor:2.24
And all the way at the bottom of build.gradle, make sure you've got apply plugin: 'kotlin-kapt'
Note: If you wanted to use Dagger in androidTest, you'd need kaptAndroidTest for the compiler/annotation-processor too.
Initial Setup
First, let's talk about at what point you begin actually adding Dagger-Android to your new app.
I like to add it at the point I've already made a ViewModel (or a presenter) and I've got the activity sitting there waiting for it.
It's because if you're about to set up the infrastructure to inject objects, you've got to have the classes that you'll be injecting first!
So, let's say you've got your new app sitting there. And have made a CitiesPresenter
, let's go about injecting it!
I like to add things in this order because you always have all the classes and info you need for the next step.
Note: you won't be able to see the injection in action until all steps are done so the order doesn't strictly matter.
-
For the Retrofit instance, and Room or Realm database. Note: this is isn't for the Apis or Data Access Objects. Those will be created later and separately. This is just for the main network and db instances.
-
Create a module for the MainActivity
For only dependencies that just this activity/fragment needs.
There will be one of these modules per activity/fragment and these split the code for your features away from each other so it's not all crammed into one module. -
For the objects that are global and related to the app, most commonly casting the Application to Context or other Android specific things.
-
This is unique to dagger-android and generates subcomponents for you.
-
Create the FragmentBuilders (if you inject into fragments)
Also unique to dagger-android and generates components for the fragments dependent on the activity components. Further constraining their scope.
-
Create the AppComponent that will bind everything together.
The CoreComponent that tells dagger how to put everything together and provides the Application inject point. (other injects are auto-generated in their subcomponents)
-
Create the Application that will initialize the AppComponent.
A regular Application that extends DaggerApplication and keeps the injection object that holds the references (injection graph) for all other objects you'd need to inject.
At this point the setup is actually done! We do need to do one more thing to inject into an activity though.
Global Modules
The global modules are ones that you'd generally only want one of and they could be used anywhere in your app.
Things like the Retrofit instance you use, or the source of your Database itself. This can include anything else that fits that definition. These two are only common examples.
Here's a NetworkModule
@Module
object NetworkModule {
@Singleton // We only want one retrofit instance at any time.
@JvmStatic // All provides methods should be static (dagger best practises)
@Provides // Tells Dagger that the Retrofit instance will come from here
fun provideRetrofit(): Retrofit =
Retrofit.
Builder()
.baseUrl("https://api.apixu.com/v1/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
You may choose to pass in an okhttpinstance in the same module and that would be right to do in here because it's needed for all Retrofit instances.
Again notice how we aren't making API objects here, only the main retrofit instance. Nor are we making the RoomDao objects, just the main database.
Also apart from being JvmStatic, as mentioned in Dagger best practises, this provider is a Singleton since we always want the same Retrofit instance everywhere in the app (to pool network resources as mentioned in Retrofit docs).
Here's a database module:
@Module
object DatabaseModule {
@JvmStatic
@Singleton
@Provides
fun getDatabaseModule(context: Context): EmotionDatabase = Room.databaseBuilder(
context,
EmotionDatabase::class.java, "emotion_database"
).build()
}
Similarly you can set up the Database Module, feel free to add or remove whatever you need here.
Module for the MainActivity
Modules are like classes that have the single responsibility of providing everything that its Activity/Fragment specifically needs.
This means that we're not going to be creating Retrofit here, neither are we going to be creating the database.
But here, we could make the NetworkApi's and DatabaseDao's for specific actions that are required by the MainActivity.
Here's an example of a Module, it takes the retrofit, and converts it to an api, it takes the database and returns the appropriate dao.
It creates a repository object, that's going to hold both of these, then constructs the Presenter by giving that to it.
It's very nearly what we did before, just completely separated from where it's actually used.
This may vary, you not always choose to use a repository, maybe you'll inject a ViewModel instead of a presenter, I've just used this to simplify.
If you want to see how a viewmodel may be injected, look through this project.
@Module
object MainActivityModule {
@JvmStatic
@Provides
fun provideEmotionApi(retrofit: Retrofit): EmotionApi = retrofit.create(EmotionApi::class)
@JvmStatic
@Provides
fun provideEmotionDao(database: EmotionDatabase): EmotionDao = database.getEmotionDao()
@JvmStatic
@Provides
fun provideRepository(emotionDao : EmotionDao, emotionApi : EmotionApi): CitiesRepository = CitiesRepository(emotionDao, emotionApi)
@JvmStatic
@Provides
fun providePresenter(repository : CitiesRepository) : Presenter = CitiesPresenter(repository)
}
The only thing this class is responsible for is constructing the presenter and any intermediate steps.
That's all any module does.
The more we contain what they do, the better isolated our code becomes and the fewer things have to change when a feature changes or is added.
Note: According to Dagger best practises, modules should only expose static methods, which is why we're using object here for the MainActivityModule and annotating the Provides methods with JvmStatic.
AppModule
The App Module contains all the android related global objects you might need. This is where you'd get your Application and Application context injected from.
Since here we're casting the injected Application to various other specific objects, using @Binds
is the best way to do it.
@Module
abstract class AppModule {
@Binds
abstract fun bindApplication(app: DaggerApplication): Application
@Binds
abstract fun bindAppContext(app: DaggerApplication): Context
}
ActivityBuilders
The ActivityBuilder (it can be called anything but this is convention) is an abstract class that will tell dagger-android which activities to tie to which modules.
We created one module for MainActivity, so we want to tell Dagger-Android that this is how we want that activity to be injected.
Behind the scenes, this generates subcomponents for them.
If you had just the one activity, no fragments and wanted to tie the MainActivityModule to the MainActivity, here's how you do it.
@Module
abstract class ActivityBuilderModule {
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun bindActivities(): MainActivity
}
If you're interested in further behind the scenes, this is the class that Dagger-Android generates for you, this is what you no longer have to think about.
@Module(subcomponents = ActivityBuilderModule_BindActivities.MainActivitySubcomponent.class)
public abstract class ActivityBuilderModule_BindActivities {
private ActivityBuilderModule_BindActivities() {}
@Binds
@IntoMap
@ClassKey(MainActivity.class)
abstract AndroidInjector.Factory<?> bindAndroidInjectorFactory(
MainActivitySubcomponent.Factory builder);
@Subcomponent(modules = {MainActivityModule.class})
public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
@Subcomponent.Factory
interface Factory extends AndroidInjector.Factory<MainActivity> {}
}
}
FragmentBuilders
If you wanted fragments too, the fragments get their own builder. Which looks like this:
@Module
abstract class FragmentBuilderModule {
@ContributesAndroidInjector(modules = [EmotionListFragmentModule::class])
abstract fun provideEmotionFragment(): EmotionListFragment
@ContributesAndroidInjector(modules = [EmotionJourneyModule::class])
abstract fun provideEmotionJourneyFragment(): EmotionJourneyFragment
}
And now you'd need to change the ActivityBuilderModule to reference them, like so:
@Module
abstract class ActivityBuilderModule {
@ContributesAndroidInjector(modules = [MainActivityModule::class, FragmentBuilderModule::class])
abstract fun bindActivities(): MainActivity
}
Notice that the FragmentBuilderModule is passed to the list of modules that the MainActivity will use.
This keeps your separation intact even if you go with a fragment approach and further divides your code into only the scope that it will be used.
AppComponent
The AppComponent is the entry point to injection. It provides the application that will send itself into the list of objects and actually create all the other objects involved when required.
This object will be held onto in the Application, where other activities and fragments can be injected.
It looks like this:
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
ActivityBuilderModule::class,
FragmentBuilderModule::class,
NetworkModule::class,
DatabaseModule::class,
AppModule::class]
)
interface AppComponent : AndroidInjector<DaggerApplication> {
override fun inject(app: DaggerApplication)
@Component.Factory
interface Builder {
fun create(@BindsInstance app: DaggerApplication): AppComponent
}
}
The AppComponent is a Singleton that will be maintained throughout your app lifecycle. It'll create and destroy dependencies as required and manage scoping for you.
All you have to give it are the globals.
The individual activity modules are contained within the activity builder.
You'll notice here we have the primary inject point, this is where the Application will pass itself in and start creating everything else.
For this, we use a Factory that will convert our DaggerApplication, into an AppComponent. To just keep a reference to the component itself, we annotate it with @BindsInstance.
We use BindsInstance to achieve "hold onto this object" in Components, and Binds to do the same in Modules.
Creating the Application
The Application was a standard android Application subclass, we're instead going to subclass DaggerApplication.
This provides a lot of the boilerplate for injection but if you can't extend this subclass you can instead implement the HasAndroidInjector
interface and go look at the implementation of DaggerApplication to see how to implement it yourself in your custom subclass.
class EmotionWheelApplication : DaggerApplication() {
private val injector by lazy {
DaggerAppComponent.factory().create(this)
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> = injector
}
You will need to build the app when you're done with this, because the DaggerAppComponent is only generated by Dagger as a build step.
Until you build, by hitting ctrl+9, the file will not exist since Dagger hasn't had a chance to build it yet.
Remember to specify that the application be used, by declaring its name in your app's manifest.
<application
android:name=".EmotionWheelApplication"
Create Subclassed Activities
As we referenced earlier, now we can just change our MainActivity from extending AppcompatActivity to DaggerAppCompatActivity!
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var presenter : Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
And that's it! The Presenter will be injected and you're ready to go!
Expanding Onto New Features
Now, whenever you create a new activity, with a new feature. Let's say HappiestCitiesActivity with its own logic in the HappiestPresenter.
- Subclass the activity to DaggerAppCompatActivity.
- Create a di subfolder under your package happiestcities.
- In that di subfolder create your HappiestCitiesModule and expose all the apis daos and presenter that you'd need.
- Add the HappiestCitiesActivity to the ActivityBuilder the way the MainActivity is.
- If you have fragments in it, add them to the fragmentbuilder.
- Annotate the presenter with lateinit var and inject!
That's a lot of setup and steps we had to follow initially, but you'll find it greatly speeds up your development and code cleanliness now that you have the initial framework ready.
Adding new modules is less work and becomes better with practise and understanding.
Even if you can't subclass Application, Activity or Fragment to DaggerApplication, DaggerAppCompatActivity, or DaggerFragment, you can click into the implementation of those classes and copy out the same interfaces and boilerplate into your classes and go right ahead.
Next Steps
Now that you've learned how to put together and use Dagger in your project, practise!
Once you've got that down, you can look into how Subcomponents, dependent components work.
There's more that can help you optimize the memory everything takes, decrease load time with Lazy loaded dependencies and more. For now, take time to celebrate that you've reached one major milestone in better Android development!
Drawbacks:
- Multi Module injection is something I haven't tried with this and it seems like it might be rocky. Take a look at this talk for a different approach that could vastly help with this.
I hope this has been helpful and do reach out with any questions that occur to you, in the comments, so I can improve upon this article.
If you want help to setup your own production environment or see how to extend this to using MockWebServer in your espresso tests, that's going to be my next article.
You can hire me as an Android consultant to improve your team's code, testing and architecture.
I'm also a 7 year experienced Android Dev and I'm looking for my next fulltime role if it's for the right company. So reach out to talk about it!
Top comments (0)