Advanced Dagger in MVVM for Android Beginners

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.

Contents

  1. Features and library that we’re going to use
  2. The app that we’re going to refactor
  3. Get started
  4. Quick tour
  5. Iteration #1: Refactoring instantiations using basic Dagger approach
  6. Testing Iteration #1: Confirm that dependencies in this iteration are singletons
  7. Iteration #2: Control the lifetime of our dependencies using subcomponents
  8. Testing Iteration #2: Confirm that dependencies have the same lifetime with their respective Activities
  9. Iteration #3: Reduce boilerplate code by using Dagger Android
  10. Testing Iteration #3: Confirm that the behavior is still the same with iteration #2
  11. Final thoughts
  12. Source code (check branches)

Features and library that we’re going to use

@Subcomponent

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.

@Subcomponent.Builder

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.

Dagger Android

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

The app that we’re going to refactor is a simple app that connects to Github API. The features are:

  • Show the full name of the user
  • Show the user’s public repositories

Our goal is to refactor the app to use Dagger.

Get started

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.

Quick tour

UserDetailsActivity & ReposActivity

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)

    ...
}

Iteration #1: Refactoring instantiations using basic Dagger approach

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.

Testing Iteration #1: Confirm that dependencies in this iteration are singletons

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.

Iteration #2: Control the lifetime of our dependencies using subcomponents

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.

Testing Iteration #2: Confirm that dependencies have the same lifetime with their respective Activities

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.

Iteration #3: Reduce boilerplate code by using Dagger Android

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.

Testing Iteration #3: Confirm that the behavior is still the same with iteration #2

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.

Final thoughts

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

7 ways to become a really good Android developer