How to implement Dependency Injection in your app with Dagger 2

How to implement Dependency Injection in your app with Dagger 2

Dependency injection code

Dependency injection (DI) is an essential technique for building modular and testable apps. Instead of directly instantiating dependencies, classes request them from external sources.

This has multiple benefits:

  • Removes tight coupling between classes
  • Increases flexibility when refactoring code
  • Enables mocking of dependencies to simplify testing

Use cases where dependency injection shines:

  • Swapping databases or API clients
  • Alternating between production vs staging services
  • Stubs for testing edge cases
  • Runtime selection of algorithms or strategies

Dagger 2 is currently the most popular open-source DI framework on Android with over 18K GitHub stars. First released by Square in 2012 and now maintained by Google.

In this comprehensive guide for Java and Kotlin developers, you will learn:

  • Core concepts of dependency injection
  • Manual dependency injection on Android
  • Introduction to Dagger 2 framework
  • Step-by-step integration for Android apps
  • Architecting complex dependency graphs
  • Testing and edge cases

Let‘s get started!

A case for dependency injection

Consider an app that fetches user data from a web API:

class UserRepository {

  private val api = WebApi()  

  fun fetchUser(id) {
    val userData = api.getUserData(id)
    // return User 
  }

}

UserRepository directly instantiates WebApi inside its constructor. This tightly couples the two classes together.

What if we need to:

  • Mock the API for testing?
  • Implement caching by adding a DB layer?
  • Support multiple API formats like JSON and XML?

We would have to keep changing the internals of UserRepository!

This gets messy as the code grows and makes testing difficult.

Dependency injection helps by externalizing dependency creation from usage.

class UserRepository(private val api: WebApi) {

  fun fetchUser(id) {
    val userData = api.getUserData(id) 
    // return User
  }

}

class App {

  fun main() {
    val api = Retrofit.createWebApi()
    val repo = new UserRepository(api)
  }

}

Now UserRepository receives the WebApi instance from outside instead of instantiating it directly.

This pattern makes it possible to reuse UserRepository across environments by injecting different WebApi implementations without changing a single line of code!

Manual dependency injection in Android

Before we dive into Dagger 2 concepts, let‘s first apply dependency injection manually within an Android app.

Here is a NewsFeed class that loads articles from a NewsApi:

class NewsFeed {

    // Tightly coupled dependency
    private val api = NewsApi()  

    fun loadArticles() {
        val articles = api.fetchLatest() 
        showArticles(articles)
    }

}

We can refactor this to use dependency injection:

class NewsFeed(private val api: NewsApi) {

    fun loadArticles() {
        val articles = api.fetchLatest()
        showArticles(articles)
    }

}

class NewsApp : Application() {

    val api = NewsApi()
    val feed = NewsFeed(api)

}

By injecting NewsApi via the constructor, we can reuse NewsFeed in multiple contexts without modifying it:

// Fetch production data 
val api = NewsApi()
val feed = NewsFeed(api)

// Mock data for testing 
val mockApi = MockNewsApi() 
val feed = NewsFeed(mockApi) 

This keeps each class isolated and focused on its core purpose, while avoiding tight couplings across the codebase!

However, dependency injection logic gets complex fast. This warrants introducing a dedicated framework like Dagger to manage dependencies declaratively.

Understanding Dagger 2

Dagger 2 is a compile-time dependency injection framework for Java and Android.

Rather than reflection or runtime code generation – analysis happens at compile time.

Dagger 2 architecture

Here is how Dagger 2 achieves dependency injection:

@Module

Modules define methods that provide dependencies.

@Component

Components consume modules to inject dependencies into constructors.

@Inject

Request injection by annotating constructors, fields, methods.

No reflection required! Dependency wiring happens entirely through code generation.

This makes Dagger very efficient and enables use of ProGuard without impacting functionality.

Over 27% of the top 1,000 Android apps use Dagger – including Google, Facebook, Pinterest, Tencent and Square.

So if you are building apps that follow best practices – adding Dagger helps future proof code for efficiency and scaling.

Next, let‘s walk through integrating Dagger step-by-step!

Set up Dagger 2

To use Dagger in your Android project:

1. Add dependencies:

// Dagger core 
implementation ‘com.google.dagger:dagger:2.x‘

// Android extensions
implementation ‘com.google.dagger:dagger-android:2.x‘

// Annotation processor
kapt ‘com.google.dagger:dagger-compiler:2.x‘

2. Create Application class

class App : Application()

3. Register Application in manifest

<application 
    android:name=".App">
</application>

This Application class will bootstrap our dependency graph.

Building a dependency graph

Components connect modules into an injectable graph.

Let‘s define the modules and component first.

@Module
Encapsulate categories of dependencies:

@Module
object NetworkModule {

    @Provides 
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient()
    }

}

@Module
object DatabaseModule {

    @Provides
    fun provideDb(app: Application): AppDatabase {
        return Room.databaseBuilder(app, AppDatabase::class.java, "news.db").build()
    }

}

@Component
Bridge modules to inject dependencies:

@Component(modules=[NetworkModule::class, DatabaseModule::class])
interface AppComponent {

    fun inject(app: Application)

    fun inject(newsActivity: NewsActivity)

}
  • modules attribute links required modules
  • inject() enables injection into listed classes

Initialize dependency graph

Creating the graph inside Application class:

class App : Application() {

    lateinit var appComponent: AppComponent

    override fun onCreate() {

        appComponent = DaggerAppComponent.builder()
            .build()

        appComponent.inject(this)

    }

}  

Where DaggerAppComponent uses our @Component interface to generate implementation code.

Calling .inject(this) provides dependencies to the Application class.

Consuming dependencies

Classes must request injection by component before using dependencies:

class NewsActivity : AppCompatActivity() {

    @Inject lateinit var db: AppDatabase

    override fun onCreate() {
        appComponent.inject(this)

        // Use db instance 

    }

}

And that‘s it! Dagger will provide injected dependencies defined in the modules.

This keeps code loosely coupled and testable by extracting dependencies completely from usage.

Lazy-loading dependencies

By default, Dagger instantiates all dependencies upfront with @Inject constructor injection.

This causes slow startup times in large apps.

Lazy-loading helps optimize this by waiting until dependencies are actually needed before instantiating them.

Lazy injection example:

class NewsActivity : AppCompatActivity() {

    @Inject lateinit var api: NewsApi 

    private val articles: List<Article> by lazy {
        api.fetchLatest()  
    }

    override fun onStart() {
        // api instantiated only when articles accessed  
    }

}

The api instance will be created lazily once articles gets accessed initially.

Injecting providers

We can also directly inject dependency providers and manually call .get() later:

class NewsActivity @Inject constructor(
    private val apiProvider: Provider<NewsApi>  
) : AppCompatActivity() {

    override fun onStart() {
        val api = apiProvider.get() 
        api.fetchLatest()
    }

}   

So choose between:

  • @Inject lateinit for lazy non-nullable dependencies
  • Provider<T> for fine-grained control over initialization

Based on use case requirements around efficiency and thread-safety.

Scopes

Scopes control lifecycles of injected dependencies.

By default, Dagger reuse single instances of dependencies across the app.

This singleton scope provides one instance to all injection sites. Fine for stateless dependencies like APIs and utilities.

But for stateful objects like caches we need scoped instances.

Custom scopes

Use custom scopes to limit dependency instances to subsets of the app:

@Scope
@Retention(RetentionPolicy.RUNTIME)
annotation class ActivityScope

@ActivityScope
@Component(...)
interface ActivityComponent {
  ... 
}

Now activity-scoped dependencies will receive separate instances:

@ActivityScope 
class Cache @Inject constructor() {

    private val data = ...

    fun putData(key: String, value: String) {
        data[key] = value
    }

    fun getData(key: String): String = data[key] !!]

}

class SettingsActivity {

    @Inject lateinit var cache: Cache

    ...
}

class ChatActivity {

    @Inject lateinit var cache: Cache

    ...
} 

Cache will have separate data across the activities even though it‘s managed by Dagger internally.

Predefined scopes

Dagger includes some predefined scopes too:

@Singleton // Per-application graph 
@Reusable // Shared instances within a @Subcomponent

So leverage built-in scopes before defining custom ones.

Testing considerations

Dependency injection makes components extremely testable by allowing mock implementations to be injected seamlessly.

Some patterns for writing testable components with Dagger:

// Shadow real implementation  
@Inject MockNewsApi mockApi;  

// Override module binding
@Module(overrides=true) 
class TestModule {

  @Provides
  fun provideApi(): NewsApi = MockNewsApi()

}

// Create test component
@Component(modules=[TestModule::class])
interface TestComponent : AppComponent

@Test 
fun validate() {

  // Inject mock into test target 
  TestComponent.inject(newsFeed) 

  newsFeed.load()

  assert(1 == 1) 
}

So using Dagger allows stubbing dependencies completely out of production code!

For end-to-end integration tests, swap bindings instead of overriding full modules:

@Component(modules=[AppModule::class])
interface TestAppComponent : AppComponent {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(app: Application): Builder 

        fun httpClient(client: OkHttpClient): Builder

        fun build(): TestAppComponent
    }

}  

class IntegrationTest {

    @get:Rule 
    var daggerRule = DaggerRule<TestAppComponent> { /*...*/ }

    @Test
    fun endToEndTest() {
       daggerRule.component.httpClient = UnstableHttpClient()  

       // Flaky client will cause test failures
    }

}

Here custom test component selectively modifies OkHttpClient binding while reusing other dependencies from original graph.

Dagger 2 Pro Tips

Here are some advanced tips for leveraging Dagger to its fullest:

Qualifiers

Differentiate multiple bindings with the same raw type using qualifiers:

@Qualifier 
annotation class DatabaseName

@Provides
@DatabaseName("users") 
fun provideUsersDb(): Database { /* ... */ } 

@Provides 
@DatabaseName("products")
fun provideProductsDb(): Database { /* ... */ }

class Storage @Inject constructor(
  @DatabaseName("users") val usersDb: Database
)

Qualifier annotations add additional metadata to clarify dependencies at injection sites when needed.

Multibindings

Contribute sets of related bindings into collections:

@Multibinds
abstract Set<PaymentProcessor> contributePaymentProcessors()

@ContributesAndroidInjector
abstract fun contributeSquarePayment(): SquarePayment 

@ContributesAndroidInjector
abstract fun contributePaypalPayment(): PaypalPayment

...

@Inject lateinit var processors: Set<PaymentProcessor> 

Now classes can provide their own implementations without knowing about each other!

Subcomponents

Partition large components into multiple injectors:

@Subcomponent
interface AuthComponent {

  fun inject(loginActivity: LoginActivity) 

}

@Component(modules = [AppModule::class, AuthModule::class])
interface AppComponent {

  fun authComponent(): AuthComponent

}

class LoginActivity : AppActivity() {

  // Only uses auth bindings  
  authComponent().inject(this)

}

Helps narrow exposure of dependencies only to relevant parts of the app.

So explore advanced Dagger recipes to optimize dependency graph architecture!

Alternative frameworks

Although Dagger is the most popular Java dependency injection framework today, here are some alternatives worth evaluating:

Koin – Ultra lightweight Kotlin-first DI library. More intuitive to use but less compile-time safety.

Kodein – Also Kotlin-centric and simple but limited in configurability.

Guice – By Google, lacks Android support. Similar API but uses reflection.

Spring – Very heavyweight but full-featured. More than just DI.

So consider tradeoffs around complexity, language support and code generation strategies while selecting a framework.

Conclusion

Dependency injection enables building apps from reusable modules wired together through clean interfaces.

Dagger 2 upholds separation of concerns through declarative annotations – with full compile-time verification and no reflection overhead.

It takes effort to model dependencies as modules and components. But pays back by keeping code loosely coupled, well-structured and easier to test at scale.

As apps grow in complexity, modular architecture matters more. So integrate Dagger today to build robust and resilient apps ready for the future!

Similar Posts