Dagger

From bibbleWiki
Jump to navigation Jump to search

Introduction

Dagger is made by Google. Dagger allows you to

  • Scope dependencies
  • Bind single instance to life cycles
  • Only need to build them once
  • Generates the code at compile time

Example Without Dagger

fun buildCar: Car =  
    Car(SturdyFrame(),
    Wheels(),
    RocketEngine())

With Dagger

fun buildCar: Car = 
    DaggerAppComponent
    .builder()
    .build()
    .buildCar()

Modules

Modules in Dagger are responsible for providing object we want to inject. They contain the methods which return the objects. Modules dagger.png
Modules are decorated with @module and the objects are decorated with @provides

@Module
fun CarModule {
    @Provides
    fun provideEngine() : Engine = Engine()

    @Provides
    fun provideFrame() : Frame = Frame()

    @Provides
    fun provideWheels() : Wheels = Wheels()

    @Provides
    fun provideCar(engine: Engine, wheels: Wheels, frame: Frame) : Car {
      return Car(frame, wheels, engine)
    }
}

To share a module across modules add the include to the modules decorator

@Module(includes = [EngineModule::class])
fun CarModule {
    @Provides
    fun provideFrame() : Frame = Frame()
...
    @Provides
    fun provideCar(engine: Engine, wheels: Wheels, frame: Frame) : Car {
      return Car(frame, wheels, engine)
    }
}

Components

A Dagger Component is something which contains a set of modules.
Dagger Components.png The component is an interface which allows access to our module instances. The underlying code is generated at build time so the interface is simple to write. We just need the @Component keyword and the modules required

@Component(modules = [
  NetworkModule::class, 
  ContextModule::class, 
  CarModule::class ])
interface AppCommponent {
   fun okHttpClient(): okHttpClient
   fun car(): Car
...
}

When creating a component we only need to list the dependencies at the top as Dagger knows to include child dependencies.
Using the component we just to the following

class SomeActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var component = DaggerAppComponent
            .builder()
            .contextModule(ContextModule(context!!)),
            .networkModule(NetworkModule()),
            .build()

        var car = component.car()
        var client = component.OkHttpClient()
    }
}

Failing to pass external dependencies will fail at runtime and not build time

Inject

Constructor Injection

The original class looked like this

class EpisodeListPresenter(
    private val episodeService: EpisodeService,
    private val schedulers: SchedularsBase,
    private val dbRepo: DbRepo)
)

Our helper function to build this class builds the component and gets the individual modules to build the EpisodeListPresenter class

fun EpisodeListFragment.buildPresenter(): EpisodeListContract.Actions {

    // Build the component
    val component = DaggerAppComponent
        .builder()
        .contextModule(ContextModule(activity?.applicationContext!!))
        .build()

    return EpisodeListPresenter(
            component.episodeService(),
            component.schedules(),
            component.dbRepo()
    )
}

To allow the dependency injection we need to

  • modify the class to add the @Inject keyword
  • modify the component to provide representative functions
  • modify the helper function to use the new functions

Modify the class

class EpisodeListPresenter @Inject constructor(
    private val episodeService: EpisodeService,
    private val schedulers: SchedularsBase,
    private val dbRepo: DbRepo)
)

Modify the Component

class AppComponent(
    private fun buildEpisodeDetailPresenter():EpisodeDetailPresenter
    private fun buildEpisodeListPresenter():EpisodeDetailListPresenter
)

Modify the Helper Function

fun EpisodeListFragment.buildPresenter(): EpisodeListContract.Actions =
    DaggerAppComponent()
        .builder()
        .contextModule(ContextModule(activity?.applicationContext!!))
        .build()
        .buildEpisodeListPresenter()

Field Injection

Field injection is when we use fields instead of classes

class EpisodeListPresenter {
    private val episodeService: EpisodeService = ...
    private val schedulers: SchedularsBase = ...
    private val dbRepo: DbRepo = ...
}

To use field inject in dagger we need to add the decorator, change the field to be public, lateinit and var.

class EpisodeListPresenter {
    @Inject lateinit var episodeService: EpisodeService
    @Inject lateinit var schedulers: SchedularsBase
    @Inject lateinit var dbRepo: DbRepo
}

In the component we need to write a function which returns the Class the field is included in e.g.

@Component(modules = [
  SchedulerModule::class, 
  DatabaseModule::class, 
  EpsisodeSerivceModule::class ])
interface AppCommponent {
   fun injectPresenter(presenter: EpisodeListPresenter)
}

We put this call into the init function of the class

class EpisodeListPresenter(context: Context) {
    @Inject lateinit var episodeService
    @Inject lateinit var schedulers
    @Inject lateinit var dbRepo

  init{
    DaggerAppComponent
     .builder()
     .contextModule(ContextModule(context))
     .build()
     .injectPresenter(this)
  } 
}

Dagger Component Layering

We we create the component with the above code, currently it is a new instance each time we do this. We need to consider the usage of the component and whether this is appropriate. Like with Angular we should look at whether this is for the whole application or just the current scope
Dagger Layering.png

Scopes

Introduction

Scopes

  • Allow us to restrict dependencies to be create once for a component
  • Must be applied to the providing function we want to restrict a well a the component
@ApplicationScope
@Component(modules = [AppModule::class])
interface AppComponent {
    fun getWarrior(): Warrior
}

@Module
class AppModule {
    private var index = 0

    @ApplicationScope
    @Provides
    fun provideWarrior(): Warrior {
        index++
        return Warrior("Warrior $index")
    }
}

Scopes only work within the same instance of a component

Access to Application Component

To provide access to an Application level Dagger component you can create an class derived from Application. The companion object is an accessor for others to use.

class App: Application() {
    lateinit var component: AppComponent
    private set

    override fun onCreate() {
        super.onCreate()
        component = DaggerAppComponent
        .builder()
        .contextModule(ContextModule(activity?.applicationContext!!))
        .build()
    } 
    companion object {
       fun getApplication(activity: Activity) = activity.application as App
    } 
}

You will need to but this in the manifest We can now change our helper function from

fun EpisodeListFragment.buildPresenter(): EpisodeListContract.Actions =
    DaggerAppComponent()
        .builder()
        .contextModule(ContextModule(activity?.applicationContext!!))
        .build()
        .buildEpisodeListPresenter()

To

fun EpisodeListFragment.buildPresenter(): EpisodeListContract.Actions =
    App.getApplication(requireActivity())
        .component.buildEpisodeListPresenter()

Application And Sub Component

Some components, an unfortunate name, need to be scoped a different levels. To aid this we can use sub components Subcomponents dagger.png
A sub component can only have one parent while a parent can be depended to by multiple components.

The parent must provide an accessor to the child component.

@ApplicationScope
@Component(modules = [AppModule::class])
interface AppComponent {
    fun warriorScreenComponent(warriorScreenModule: WarriorScreenModule) : WarriorScreenComponent
}


And the child must be annotated with the @Subcomponent decorator.

@WarriorScreenScope
@Subcomponent(modules = [WarriorScreenModule::class])
interface WarriorScreenComponent {
    fun inject(warriorActivity: WarriorActivity)
}

Creating the Activity is now straight forward.

class WarriorActivity : AppCompatActivity() {

    private val TAG = "WarriorActivity"

    @Inject
    lateinit var presenter: WarriorPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val appComponent = DaggerAppComponent.builder()
                .appModule(AppModule())
                .build()

        val warriorScreenComponent = appComponent.warriorScreenComponent(WarriorScreenModule())
        warriorScreenComponent.inject(this)
        Log.d(TAG, presenter.warrior.name)
    }
}

Simple Worked Example

This is to provide a simple usage of dagger to help understand how it works.

Define Component

This is where you specify the module(s) in the interface to use when injecting

@Component(modules = [ApiModule::class])
interface ApiComponent {
    fun inject(Service: CountriesService)
}

Define module

Define a module and the function is provides

@Module
class ApiModule {

    private val BASE_URL = "https://raw.githubusercontent.com"

    @Provides
    fun provideCountriesApi() =
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
            .build()
            .create(CountriesApi::class.java)
}

Inject into a Class

Use the component by injecting it

class CountriesService {
    @Inject
    lateinit var api: CountriesApi

    init {
        DaggerApiComponent.create().inject(this)
    }

    fun getCountries(): Single<List<Country>> = api.getCountries()
}