Basic Dagger in MVVM for Android Beginners

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.

  1. When to use Dagger’s features? - a crash course of the basic Dagger features and when to use them. You can skip this one if you’re already familiar with the basics of Dagger.
  2. How do we use Dagger in MVVM? - a hands-on exercise where we write code that doesn’t use Dagger and refactor it to use Dagger.

When to use Dagger’s features?

@Inject

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.

When to use @Inject?

  • If you want to tell Dagger that this class can be requested by another class. It means that this class is now a dependency and some class in your project needs it. As we move forward, take note of the term - dependency because it will come up often.
class Repository @Inject constructor() {}
  • If you want to tell Dagger to supply the parameters in the constructor for us. Note that RemoteSource needs to have @Inject on its constructor as well as explained in #1.
class Repository @Inject constructor(remoteSource: RemoteSource) {}
  • If you want to tell Dagger that you’re asking for a dependency and supply it in this variable.
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var repository: Repository

}

@Component

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.

When to use @Component?

  • You always use it. Without it, Dagger won’t know which classes to provide dependencies and what dependencies to provide.
  • If you expose dependencies that you want to manually get.
@Component
interface AppComponent {
    fun repository(): Repository
}
  • If you want to give Dagger the ability inject dependencies in a class’ member variables that you specified.
@Component
interface AppComponent {
    fun inject(activity: MainActivity)
}

class MainActivity : AppCompatActivity() {
    
    @Inject
    lateinit var repository: Repository
}

@Module

It makes a normal class into a Dagger Module. If Dagger Components exposes dependencies, Dagger Modules creates these dependencies.

@Module
class AppModule {}

When to use @Module?

  • If you don’t own the class, then you have to create it in your Dagger Module. A good example are third party dependencies like Retrofit and Gson.
@Module
class AppModule {

    @Provides
    fun providesRetrofit(): Retrofit {
        return Retrofit.Builder()
                .baseUrl("https://sample.com/")
                .build()
    }

    @Provides
    fun providesGson(): Gson {
        return Gson()
    }
}
  • If dagger doesn’t know how to create it. A good example is when you depend on interfaces and not on implementations.
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.

@Scope

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.

  • A dependency that is not scoped is instantiated for each class that requests it. If you have two classes that needs the same dependency, Dagger will instantiate two as well. That’s why using Dagger Scopes is very important if you them to share dependencies.

When to use @Scope?

  • If we want to give a dependency a lifetime so that we don’t have to instantiate it multiple times.

@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.

How do we use Dagger in MVVM?

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.

  1. Setup
  2. Architecture
  3. Iteration #1: Setup Github API in MainActivity
  4. Refactoring Iteration #1: Instantiating Retrofit and Api out of MainActivity
  5. Iteration #2: Move api logic out of MainActivity and into a ViewModel
  6. Refactoring Iteration #2: Instantiating MainViewModelFactory out of MainActivity
  7. Iteration #3: Move api logic out of MainViewModel into a Repository
  8. Refactoring Iteration #3: Instantiating a UserRepository out of MainViewModel
  9. Final thoughts
  10. What’s next?
  11. Source code

Setup

  1. Start by opening up your Android Studio
  2. Create a new project called “Basic Dagger Tutorial”
  3. Create an empty Activity
  4. Make sure Use androidx.* artifacts is checked
  5. Leave the name as MainActivity

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.

Architecture

We have an Activity that talks to a ViewModel and this ViewModel talks to a Repository class.

Iteration #1: Setup Github API in MainActivity

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.

Refactoring Iteration #1: Instantiating Retrofit and Api out of MainActivity

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.

Iteration #2: Move api logic out of 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.

Refactoring Iteration #2: Instantiating 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.

Iteration #3: Move api logic out of MainViewModel into a Repository

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.

Refactoring Iteration #3: Instantiating a UserRepository out of MainViewModel

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.

Final thoughts

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.

What’s next?

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!👇

7 ways to become a really good Android developer