This tutorial will cover a much more advanced way of using Dagger. Especially, controlling a dependency’s lifetime. If you’re new to Dagger, I encourage you to read my basic dagger tutorial first where you will learn the basic features of Dagger and also a hands-on exercise where you refactor code to use Dagger.
It’s a child component for a @Component. Everything in @Component can be accessed by its subcomponents. It needs to be under a @Component for it to work.
It’s as its name suggests a builder for a subcomponent. You usually expose these as methods in it’s parent @Component and then use those methods to instantiate this subcomponent.
A library that will allows us to inject dependencies in member variables for Android framework classes (e.g. Activity, Fragment, etc.).
The app that we’re going to refactor is a simple app that connects to Github API. The features are:
Our goal is to refactor the app to use Dagger.
Clone or download and open this project first - https://github.com/arthlimchiu/dagger-workshop
Run the app and enter a Github username to start using it.
These classes are where most of our refactoring will happen. If you look at their onCreate
methods, you’ll see that dependencies of these activities are instantiated in there.
override fun onCreate(savedInstanceState: Bundle?) {
...
retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
api = retrofit.create(Api::class.java)
reposRepository = ReposRepositoryImpl(api)
factory = ReposViewModelFactory(reposRepository)
viewModel = ViewModelProviders.of(this, factory).get(ReposViewModel::class.java)
...
}
override fun onCreate(savedInstanceState: Bundle?) {
...
retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
api = retrofit.create(Api::class.java)
userRepository = UserRepositoryImpl(api)
factory = UserDetailsViewModelFactory(userRepository)
viewModel = ViewModelProviders.of(this, factory).get(UserDetailsViewModel::class.java)
...
}
Create a new class called AppModule. This Dagger module is responsible for providing commonly used dependencies throughout our app.
@Module
class AppModule {
@Provides
@Singleton
fun providesRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun providesApi(retrofit: Retrofit): Api {
return retrofit.create(Api::class.java)
}
}
Create a new class called RepositoryModule. This Dagger module is responsible for providing Repository dependencies.
@Module
class RepositoryModule {
@Provides
@Singleton
fun providesUserRepository(api: Api): UserRepository {
return UserRepositoryImpl(api)
}
@Provides
@Singleton
fun providesReposRepository(api: Api): ReposRepository {
return ReposRepositoryImpl(api)
}
}
Create a new interface called AppComponent.
@Singleton
@Component(
modules = [
AppModule::class,
RepositoryModule::class
]
)
interface AppComponent {
fun inject(activity: UserDetailsActivity)
fun inject(activity: ReposActivity)
}
Build the app first before proceeding. Open your App class and instantiate your AppComponent.
class App : Application() {
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent
.builder()
.build()
}
}
lateinit var appComponent: AppComponent
Open your UserDetailsViewModelFactory class and add @Inject
to its constructor.
@Suppress("UNCHECKED_CAST")
class UserDetailsViewModelFactory @Inject constructor(private val userRepository: UserRepository) : ... {
...
}
Open your ReposViewModelFactory class and add @Inject
to its constructor.
@Suppress("UNCHECKED_CAST")
class ReposViewModelFactory @Inject constructor(private val reposRepository: ReposRepository) : ... {
...
}
Open your UserDetailsActivity class, remove the instantiations and just inject UserDetailsViewModelFactory. Be sure to add appComponent.inject(this)
.
class UserDetailsActivity : AppCompatActivity() {
@Inject
lateinit var factory: UserDetailsViewModelFactory
private lateinit var viewModel: UserDetailsViewModel
private lateinit var fullName: TextView
private lateinit var numOfRepos: TextView
override fun onCreate(savedInstanceState: Bundle?) {
appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_details)
fullName = findViewById(R.id.full_name)
numOfRepos = findViewById(R.id.num_of_repos)
viewModel = ViewModelProviders.of(this, factory).get(UserDetailsViewModel::class.java)
viewModel.user.observe(this, Observer { user ->
fullName.text = user.name
numOfRepos.text = "Public Repos: ${user.repos}"
})
val username = intent.getStringExtra("username")
viewModel.searchUser(username)
}
}
Open your ReposActivity class, remove the instantiations and just inject ReposViewModelFactory. Be sure to add appComponent.inject(this)
.
class ReposActivity : AppCompatActivity() {
@Inject
lateinit var factory: ReposViewModelFactory
private lateinit var viewModel: ReposViewModel
private lateinit var repos: RecyclerView
private lateinit var reposAdapter: ReposAdapter
override fun onCreate(savedInstanceState: Bundle?) {
appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_repos)
repos = findViewById(R.id.repos)
repos.layoutManager = LinearLayoutManager(this)
reposAdapter = ReposAdapter(listOf())
repos.adapter = reposAdapter
viewModel = ViewModelProviders.of(this, factory).get(ReposViewModel::class.java)
viewModel.repos.observe(this, Observer { repositories ->
reposAdapter.updateRepos(repositories)
})
val username = intent.getStringExtra("username")
viewModel.getRepos(username)
}
}
Run the app, test it, and behavior should stay the same.
Open your UserRepositoryImpl class and add a simple code to log the index
variable.
class UserRepositoryImpl(private val api: Api) : UserRepository {
private var index = 0
override fun getUser(username: String, onSuccess: (user: User) -> Unit, onFailure: (t: Throwable) -> Unit) {
index++
Log.d("UserRepository", "Index: $index")
...
}
}
Run the app, enter a username and click Search. Press back and click Search again. Your logs should look like this:
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 1
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 2
If Dagger created another instance of UserRepository, index would always be 1. This means that there really is only one instance of UserRepository.
This is fine for a small app but as your apps grow bigger, making everything singleton is very inefficient. For this example, an instance of UserRepository is only used in UserDetailsActivity and there only. We need to find a way to bind a lifetime to this instance such that it lives and dies together with the Activity. That’s what we’re going to do next.
Create a new Dagger scope called ActivityScope.
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope
Create a new class called UserDetailsModule.
@Module
class UserDetailsModule {
@Provides
@ActivityScope
fun providesUserRepository(api: Api): UserRepository {
return UserRepositoryImpl(api)
}
}
Create a new class called ReposModule.
@Module
class ReposModule {
@Provides
@ActivityScope
fun providesReposRepository(api: Api): ReposRepository {
return ReposRepositoryImpl(api)
}
}
Create a new interface called UserDetailsSubcomponent.
@ActivityScope
@Subcomponent(
modules = [
UserDetailsModule::class
]
)
interface UserDetailsSubcomponent {
@Subcomponent.Builder
interface Builder {
fun build(): UserDetailsSubcomponent
}
fun inject(activity: UserDetailsActivity)
}
Create a new interface called ReposSubcomponent.
@ActivityScope
@Subcomponent(
modules = [
ReposModule::class
]
)
interface ReposSubcomponent {
@Subcomponent.Builder
interface Builder {
fun build(): ReposSubcomponent
}
fun inject(activity: ReposActivity)
}
Open your AppModule class and add these subcomponents.
@Module(
subcomponents = [
ReposSubcomponent::class,
UserDetailsSubcomponent::class
]
)
class AppModule {
...
}
Open your AppComponent interface, remove RepositoryModule and inject methods and add methods that return your subcomponents’ Builder. These methods are what we’re going to use to instantiate the dependencies for each subcomponent.
@Singleton
@Component(
modules = [
AppModule::class
]
)
interface AppComponent {
fun userDetailsSubcomponent(): UserDetailsSubcomponent.Builder
fun reposSubcomponent(): ReposSubcomponent.Builder
}
Open your UserDetailsActivity class, remove appComponent.inject(this)
and instantiate your UserDetailsSubcomponent.
class UserDetailsActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_details)
appComponent
.userDetailsSubcomponent()
.build()
.inject(this)
fullName = findViewById(R.id.full_name)
numOfRepos = findViewById(R.id.num_of_repos)
...
}
}
Open your ReposActivity class, remove appComponent.inject(this)
and instantiate your ReposSubcomponent.
class ReposActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_repos)
appComponent
.reposSubcomponent()
.build()
.inject(this)
repos = findViewById(R.id.repos)
repos.layoutManager = LinearLayoutManager(this)
...
}
}
Run the app, test it, and behavior should stay the same.
Remember that in the previous testing iteration we added code that logs a variable in our UserRepositoryImpl class.
class UserRepositoryImpl(private val api: Api) : UserRepository {
private var index = 0
override fun getUser(username: String, onSuccess: (user: User) -> Unit, onFailure: (t: Throwable) -> Unit) {
index++
Log.d("UserRepository", "Index: $index")
...
}
}
Run the app, enter a username and click Search. Press back and click Search again. Your logs should look like this:
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 1
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 1
As you can see, an instance of UserRepository now gets destroyed when our UserDetailsActivity gets destroyed.
When your app grows bigger and have a lot of activities/fragments, creating a Subcomponent for each of them can be repetitive and just boilerplate code. Using Dagger Android can help us with that.
Create a new class called ActivityBuilder.
@Module
abstract class ActivityBuilder {
@ActivityScope
@ContributesAndroidInjector(modules = [UserDetailsModule::class])
abstract fun userDetailsActivity(): UserDetailsActivity
@ActivityScope
@ContributesAndroidInjector(modules = [ReposModule::class])
abstract fun reposActivity(): ReposActivity
}
Open your AppModule class and remove subcomponent declarations.
@Module
class AppModule {
...
}
Open your AppComponent interface, add AndroidInjectionModule and ActivityBuilder modules, and remove the methods that returns your subcomponents’ Builders.
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
ActivityBuilder::class
]
)
interface AppComponent {
fun inject(app: App)
}
Open your App class and implement HasAndroidInjector
. This tells to enable injection of dependencies inside an Activity. If you want to inject dependencies to Fragments, it’s Activity must implement HasAndroidInjector
as well.
It’s like a parent to child relationship where the parent must implement this interface so that we can enable injection of dependencies in its children.
class App : Application(), HasAndroidInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent
.builder()
.build()
appComponent.inject(this)
}
}
lateinit var appComponent: AppComponent
Open your UserDetailsActivity class, remove subcomponent instantiation and replace it with AndroidInjection.inject(this)
.
class UserDetailsActivity : AppCompatActivity() {
@Inject
lateinit var factory: UserDetailsViewModelFactory
private lateinit var viewModel: UserDetailsViewModel
private lateinit var fullName: TextView
private lateinit var numOfRepos: TextView
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_details)
...
}
}
Open your ReposActivitiy class, remove subcomponent instantiation, and replace it with AndroidInjection.inject(this)
.
class ReposActivity : AppCompatActivity() {
@Inject
lateinit var factory: ReposViewModelFactory
private lateinit var viewModel: ReposViewModel
private lateinit var repos: RecyclerView
private lateinit var reposAdapter: ReposAdapter
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_repos)
...
}
}
Run the app, test it, behavior should stay the same.
Run the app, enter a username and click Search. Press back and click Search again. Your logs should still look like this:
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 1
/com.arthlimchiu.daggerworkshop D/UserRepository: Index: 1
The only difference with this iteration and iteration #2 is that we eliminated the creation of Subcomponents as Dagger Android will generate this for us already.
Dagger is hard to understand at first and that’s true. It takes a lot of practice and tinkering to fully grasp what it really is but over time as you keep using it in different projects, slowly you will start to understand it and be amazed how powerful and useful it is.
I encourage you to take your time and tinker with the code. You can create more screens and create more dependencies and try connecting these together using Dagger.
I hope that this tutorial helped you even just a little bit. If you have any questions, comment them below. If you want to be updated with this kind of “real-world” tutorials you can subscribe to my newsletter below and get a free book on the 7 ways to become a really good Android developer!👇
Do You Want to Become Really Good at Android Development?
Here are 7 ways to do it👇