Skip to content

tslamic/jokster

Repository files navigation

Jokster App 👺

A simple joke-telling app demonstrating the very basics of Dagger2. 🗡️

Motivation

Dependency injection or DI is a 25-dollar term for a 5-cent concept. It means giving an object its instance variables. So instead of e.g. creating an OkHttpClient instance yourself:

class ChuckApi {
  private val client = OkHttpClient()
  // ...
}

inject it via a constructor:

class ChuckApi(private val client: OkHttpClient) { 
 // ...
}

Voilà! You just performed a dependency injection. Simple, right?

One of the cornerstones of object-oriented design is the Dependency inversion principle. In essence, it focuses on what the code does, and not how it does it. Consider the following class:

class ChuckApi {
  fun tellJoke(): String {
    // returns a funny joke from the interwebs
  }
}

If another class relies on the ChuckApi, such as:

class Repo(private val api: ChuckApi) {
 // ...
}

then any changes to the ChuckApi most likely affect the Repo, too. Also, assuming ChuckApi requires a network connection, Repo might be difficult to test. Introducing an interface such as:

interface FunnyApi {
  fun tellJoke(): String
}

and having ChuckApi implement it:

class ChuckApi : FunnyApi {
  override fun tellJoke(): String {
    // same as before
  }
}

but making the Repo depend on the interface, rather than a concrete class:

class Repo(private val api: FunnyApi) {
 // heavy use of the api here ...
}

gives us the ability to pass Repo any implementation of the FunnyApi interface. This makes testing easier and ensures Repo needs no code changes if various FunnyApi implementations are provided.

So, how do we pass FunnyApi instances to the Repo class? You guessed it - it's with dependency injection!

A vague intro to Dagger2

Dagger2 is a popular DI framework for Java, Kotlin and Android. To use it, first add it to the list of your dependencies in build.gradle:

dependencies {
  implementation 'com.google.dagger:dagger:2.x'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

When using Kotlin, use kapt instead:

apply plugin: 'kotlin-kapt'

dependencies {
  implementation 'com.google.dagger:dagger:2.x'
  kapt 'com.google.dagger:dagger-compiler:2.x'
}

To provide dependencies, you need modules. Modules are classes with the @Module annotation, and methods annotated with @Provides:

@Module  
class NetworkModule {  
    @Provides fun providesHttpClient(): OkHttpClient = OkHttpClient()
}

Dagger will inspect the class, look at the return types of the annotated methods, and say: I now know how to create these objects!

Another method might need an OkHttpClient to create a dependency:

@Module  
class ApiModule {  
    @Provides fun providesFunnyApi(c: OkHttpClient): FunnyApi = ChuckApi(c)
}

Assuming Dagger knows about the NetworkModule, it knows how to create the OkHttpClient and therefore knows how to create an instance of FunnyApi. In cases where Dagger can infer all the necessary dependencies, you can use a shorter @Binds annotation:

@Module  
interface ApiModule {  
    // Equivalent to the ApiModule above, just shorter. 
    @Binds fun providesFunnyApi(api: ChuckApi): FunnyApi
}

To let Dagger create objects on your behalf, you need to add an @Inject annotation to your constructors:

class ChuckApi @Inject constructor(c: OkHttpClient) : FunnyApi {  
    // ...
}

Components stitch multiple modules together:

@Component(  
    modules = [  
        NetworkModule::class,  
        ApiModule::class  
  ]  
)  
interface AppComponent {  
    fun inject(activity: MainActivity)
    fun client(): OkHttpClient
}

Note the @Component annotation, along with the list of modules it includes.

Dagger will generate a class called DaggerAppComponent implementing the AppComponent interface. To consume the dependencies, you need a DaggerAppComponent instance, which is typically created in an Application class:

class App : Application() {  
    private lateinit var component: AppComponent  
  
    override fun onCreate() {  
        super.onCreate()  
        component = DaggerAppComponent.builder()  
            .networkModule(NetworkModule())  
            .apiModule(ApiModule())  
            .build()  
    }
  
    fun component(): AppComponent = component  
}

You're now ready to consume dependencies. There are two ways to do it.

  1. Use an @Inject annotation:
class MainActivity : AppCompatActivity() {  
    @Inject lateinit var factory: JokerViewModel.Factory  
  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  

        // get the AppComponent instance, and invoke the 
        // appropriate inject method which will automatically
        // populate all @Inject fields.
        val component = (application as App).component()  
        component.inject(this)
    }
}

Note that the inject method is not magical, nor special. The name is merely a convention, we could easily call it awesomeMethod4Realz. The important bit is the parameter - MainActivity. Dagger will generate a class that knows how to populate all @Inject fields, and whenever component.inject(this) is invoked, the call will be propagated to a method looking like this:

public static void injectFactory(MainActivity instance, JokerViewModel.Factory factory) {  
  instance.factory = factory;  
}

As you may have guessed, the JokerViewModel.Factory is created and provided by Dagger.

  1. Call the appropriate accessor methods:
class MainActivity : AppCompatActivity() {    
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  

        // get the AppComponent instance and invoke the getter.
        val component = (application as App).component()  
        val client = component.client()
    }
}

Use this repo to play around with Dagger2, and check out these links to learn more:

License

Copyright © 2019 tslamic
This work is free. You can redistribute it and/or modify it under the
terms of the Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar.

About

A demo app used for Dagger2 workshops.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages