This post doesn’t answer the questions of what and why but how and when. If you want to know what is Dagger and why we use it, head over to their official documentation. If you want to know when to use its features and how to use it, then this post will help you on that.
This tutorial is divided into two sections which are independent of each other.
It is placed before the constructor and exposes the class to Dagger so that it can do something with it. It can also be placed on a variable that you need and let Dagger supply it for you.
class Repository @Inject constructor() {}
@Inject
on its constructor as well as explained in #1.class Repository @Inject constructor(remoteSource: RemoteSource) {}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: Repository
}
It makes a normal interface into a Dagger Component. It is an interface that exposes dependencies that can be requested and the classes that request these dependencies such as Activities/Fragments in Android.
@Component
interface AppComponent {
fun repository(): Repository
}
@Component
interface AppComponent {
fun inject(activity: MainActivity)
}
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: Repository
}
It makes a normal class into a Dagger Module. If Dagger Components exposes dependencies, Dagger Modules creates these dependencies.
@Module
class AppModule {}
@Module
class AppModule {
@Provides
fun providesRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://sample.com/")
.build()
}
@Provides
fun providesGson(): Gson {
return Gson()
}
}
interface RemoteSource
class RemoteSourceImpl @Inject constructor() : RemoteSource {}
class Repository @Inject constructor(remoteSource: RemoteSource) {}
Repository asks Dagger to supply me a RemoteSource but the problem is - Dagger doesn’t know how to create it. It only knows how to create RemoteSourceImpl. That’s the time you need a Dagger Module to provide an implementation of RemoteSource.
@Module
class RemoteSourceModule {
@Provides
fun providesRemoteSource(): RemoteSource {
return RemoteSourceImpl()
}
}
Now, Dagger knows how to create a RemoteSource by providing its implementation class.
It gives a dependency a lifetime. In this post we will only focus on making a dependency live for the duration of the whole app. We will use a built-in scope that comes with Dagger - @Singleton
.
Sometimes developers misunderstand this scope because they think it will magically make their classes singleton but it won’t because it’s just a normal scope like any other scope that you can create.
@Module
class RemoteSourceModule {
@Provides
@Singleton
fun providesRemoteSource(): RemoteSource {
return RemoteSourceImpl()
}
}
@Singleton
@Component
interface AppComponent {
fun inject(activity: MainActivity)
}
Now that we tied our dependency to our component by using the @Singleton
scope, RemoteSourceImpl will live until AppComponent gets destroyed. You’ll see more of this later on.
The app that we’re going to make is a simple app that calls the Github API to search for a user using a username. Our development workflow is very simple. We write the code without using Dagger first and then we refactor the code and use Dagger.
Open your app-level build.gradle file and import these dependencies
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
...
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Support
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// ViewModel and LiveData
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
// Dagger
// https://github.com/google/dagger
implementation 'com.google.dagger:dagger:2.24'
kapt 'com.google.dagger:dagger-compiler:2.24'
// Retrofit
// https://github.com/square/retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
// Gson
// https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.8.5'
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
Add <uses-permission android:name="android.permission.INTERNET"/>
in your AndroidManifest.xml.
We have an Activity that talks to a ViewModel and this ViewModel talks to a Repository class.
Create a data class User.
data class User(
@SerializedName("name") val name: String
)
Open up activity_main.xml.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/full_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"/>
<EditText
android:id="@+id/username"
android:hint="Enter Github username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="Search"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Create a new interface called Api.
interface Api {
@GET("users/{user}")
fun getUser(@Path("user") user: String): Call<User>
}
Open up your MainActivity.
class MainActivity : AppCompatActivity() {
private lateinit var fullName: TextView
private lateinit var username: EditText
private lateinit var search: Button
private lateinit var retrofit: Retrofit
private lateinit var api: Api
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fullName = findViewById(R.id.full_name)
username = findViewById(R.id.username)
search = findViewById(R.id.search)
retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
api = retrofit.create(Api::class.java)
}
override fun onStart() {
super.onStart()
search.setOnClickListener {
searchUser(username.text.toString())
}
}
private fun searchUser(username: String) {
api.getUser(username).enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
response.body()?.let { user ->
fullName.text = user.name
}
}
override fun onFailure(call: Call<User>, t: Throwable) {
Log.e("MainActivity", "onFailure: ", t)
}
})
}
}
Run the app and enter a Github username and confirm that it shows the name. You can use my username - arthlimchiu.
Create a new class called AppModule.
@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)
}
}
You might be wondering about the providesApi(retrofit: Retrofit)
method. Anything that Dagger knows how to instantiate, you can pass it as parameter to your method in your Dagger Module as well.
Create a new interface called AppComponent.
@Singleton
@Component(
modules = [
AppModule::class
]
)
interface AppComponent {
fun inject(activity: MainActivity)
}
What we’re doing here is the third item of “When to use @Component?”.
If you want to give Dagger the ability inject dependencies in a class’ member variables that you specified.
And the first item of “When to use @Scope?”.
If we want to give a dependency a lifetime so that we don’t have to instantiate it multiple times.
Marking our dependencies with @Singleton
scope to be the same with our AppComponent ties our dependencies’ lifetime with AppComponent’s . Knowing when a Dagger Component gets destroyed depends on where you instantiated it. Since we want AppComponent to live for the whole duration of the app, we instantiate it in our Application class.
Make sure to build the app first before proceeding.
Create a new class called App.
class App : Application() {
override fun onCreate() {
super.onCreate()
component = DaggerAppComponent
.builder()
.build()
}
}
lateinit var component: AppComponent
DaggerAppComponent is a generated class by Dagger by prefixing your component name with “Dagger”. It’s important to build it first so that Dagger will generate this class and you can reference it.
Open your AndroidManifest.xml and reference your newly created App class.
<manifest ....>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".App"
....>
....
</application>
</manifest>
Open your MainActivity and remove Retrofit and Api instantiation.
class MainActivity : AppCompatActivity() {
....
@Inject
lateinit var api: Api
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
component.inject(this)
fullName = findViewById(R.id.full_name)
username = findViewById(R.id.username)
search = findViewById(R.id.search)
}
....
}
Run the app and test if it works as intended. Nothing has changed except that it’s no longer MainActivity’s responsibility to instantiate Retrofit and our Api.
Next, we’re going to move out the api logic out of our MainActivity and into a ViewModel.
Create a new class called MainViewModel and extend ViewModel.
class MainViewModel(private val api: Api) : ViewModel() {
private val _fullName = MutableLiveData<String>()
val fullName: LiveData<String>
get() = _fullName
fun searchUser(username: String) {
api.getUser(username).enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
response.body()?.let { user ->
_fullName.value = user.name
}
}
override fun onFailure(call: Call<User>, t: Throwable) {
Log.e("MainActivity", "onFailure: ", t)
}
})
}
}
Create a new class called MainViewModelFactory.
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory(private val api: Api) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(api) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
ViewModelProvider.Factory is required if you want to pass objects in a ViewModel’s constructor. Knowing why is out of the scope of this tutorial. Check out their official documentation to learn more.
Open your MainActivity, remove searchUser()
method and use your MainViewModel.
class MainActivity : AppCompatActivity() {
....
@Inject
lateinit var api: Api
private lateinit var viewModel: MainViewModel
private lateinit var factory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
component.inject(this)
factory = MainViewModelFactory(api)
viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)
....
viewModel.fullName.observe(this, Observer { name ->
fullName.text = name
})
}
override fun onStart() {
super.onStart()
search.setOnClickListener {
viewModel.searchUser(username.text.toString())
}
}
}
Run the app and test if it works as intended.
Now that we’ve move out the logic of calling the api, let’s refactor it and instantiate MainViewModelFactory out of MainActivity.
Open your MainViewModelFactory and annotate it with @Inject
.
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory @Inject constructor(private val api: Api) : ViewModelProvider.Factory {
....
}
Create a new class called ViewModelModule.
@Module
class ViewModelModule {
@Provides
fun providesMainViewModelFactory(api: Api): MainViewModelFactory {
return MainViewModelFactory(api)
}
}
Note that we didn’t tie our MainViewModelFactory’s lifetime with our AppComponent. We don’t want this to live for the whole app.
Open your AppComponent and add ViewModelModule to its array of modules.
@Singleton
@Component(
modules = [
AppModule::class,
ViewModelModule::class
]
)
interface AppComponent { .... }
Open your MainActivity. Remove factory = MainViewModelFactory(api)
and lateinit var api: Api
.
class MainActivity : AppCompatActivity() {
private lateinit var fullName: TextView
private lateinit var username: EditText
private lateinit var search: Button
@Inject
lateinit var factory: MainViewModelFactory
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
component.inject(this)
viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)
fullName = findViewById(R.id.full_name)
username = findViewById(R.id.username)
search = findViewById(R.id.search)
viewModel.fullName.observe(this, Observer { name ->
fullName.text = name
})
}
....
}
Run the app and test if it works as intended. Slowly you start to see the power of Dagger in it’s simplest approach. It’s no longer a class’ responsibility to instantiate its dependencies but Dagger’s.
Talking to data sources such as the API or database are responsibilities of a Repository class. Let’s move the api logic again and put it inside a Repository class.
Create a new interface called UserRepository.
interface UserRepository {
fun getUser(username: String, onSuccess: (user: User) -> Unit, onFailure: (t: Throwable) -> Unit)
}
Create a new class user UserRepositoryImpl.
class UserRepositoryImpl(private val api: Api) : UserRepository {
override fun getUser(username: String, onSuccess: (user: User) -> Unit, onFailure: (t: Throwable) -> Unit) {
api.getUser(username).enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
response.body()?.let { user ->
onSuccess.invoke(user)
}
}
override fun onFailure(call: Call<User>, t: Throwable) {
onFailure.invoke(t)
}
})
}
}
Open your MainViewModel, instantiate UserRepositoryImpl and remove api logic.
class MainViewModel(private val api: Api) : ViewModel() {
private val _fullName = MutableLiveData<String>()
val fullName: LiveData<String>
get() = _fullName
private var userRepository: UserRepository = UserRepositoryImpl(api)
fun searchUser(username: String) {
userRepository.getUser(
username,
{ user -> _fullName.value = user.name },
{ t -> Log.e("MainActivity", "onFailure: ", t) }
)
}
}
Run the app and test if it works as intended.
If you’ve noticed, MainViewModel still has reference to Api because we need to instantiate a UserRepository. Next, let’s refactor it and instantiate a UserRepository outside of MainViewModel and remove a reference to Api as well.
Create a new class called RepositoryModule.
@Module
class RepositoryModule {
@Provides
@Singleton
fun providesUserRepository(api: Api): UserRepository {
return UserRepositoryImpl(api)
}
}
Open your AppComponent and add the new module that you’ve just created.
@Singleton
@Component(
modules = [
AppModule::class,
ViewModelModule::class,
RepositoryModule::class
]
)
interface AppComponent { .... }
Open your MainViewModel, change your constructor to receive a UserRepository, and remove its instantiation.
class MainViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _fullName = MutableLiveData<String>()
val fullName: LiveData<String>
get() = _fullName
fun searchUser(username: String) {
userRepository.getUser(
username,
{ user -> _fullName.value = user.name },
{ t -> Log.e("MainActivity", "onFailure: ", t) }
)
}
}
Open your MainViewModelFactory, change your constructor to receive a UserRepository, and pass it to MainViewModel.
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory @Inject constructor(private val userRepository: UserRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(userRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Open your ViewModelModule and instead of receiving an Api, change it to UserRepository and pass it to MainViewModelFactory.
@Module
class ViewModelModule {
@Provides
fun providesMainViewModelFactory(userRepository: UserRepository): MainViewModelFactory {
return MainViewModelFactory(userRepository)
}
}
Run the app and test if it works as intended. As you can see, dependencies are now instantiated outside of our dependent classes and instead provided in their constructors.
I made this tutorial to be as beginner friendly as possible which is why we only use the basic features of Dagger. I encourage you to give it a run through again until you familiarize yourself with how things are wired up.
Dagger is one of the essential tools of an Android developer nowadays. It’s complicated at first if you’re new to the concept of Dependency Injection.
I hope that this tutorial helped you understand Dagger even if it’s just a little bit. If you have any questions let me know in the comments.
We’ve learned how to use Dagger in its simplest approach. But this is just the tip of iceberg. There’s more to Dagger than this.
Now that you’ve done the basic approach, this let’s do a much more advanced approach of Dagger.
If you want to be notified when this new advanced Dagger tutorial is released, 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👇