Lesson 07 CLEANer Code ‐ Dependency Injection - FranGarc/LearningPath GitHub Wiki

Topic: CLEANer Code: Dependency Injection.

We call a Dependency to the relationship between two software components - i.e. classes. When Class A needs of a property of method from Class B, we say Class A is a client of Class B - which also is called service -, and also that Class A has a dependency on Class B.

Dependency Injection (DI) is a technique in which an object (say Class A) receives the objects, properties, or functions that it needs (say Class B).

Dependency injection is used to make a class independent of its dependencies or to create a loosely coupled program. Dependency injection is useful for improving the reusability of code. Likewise, by decoupling the usage of an object, more dependencies can be replaced without needing to change class. All this aligns with the SOLID principles you researched in lesson 5.

Task: Implement Dependency Injection with Dagger Hilt.

Hilt for short - is a library that provides a way to use DI in your Android application by providing containers for every Android class in your project and managing their lifecycles automatically.

Hilt is built atop Dagger, a very popular DI library.

Your task here is to improve the CLEANness of your code by using Dagger Hilt. This will require some new code files, changes in current files and some refactor where you notice its due.

Requisites

Refactor

  • PokemonServiceImpl has a dependency in PokeApi. Reformat the constructor so that it receives this dependency as a private val parameter.
  • RemoteDataSource has a dependency in PokemonService. Reformat the constructor so that it receives this dependency as a private val parameter.
  • The UseCases have a dependency in DatasourceAdapter. Reformat the constructor so that it receives this dependency as a private val parameter.

Dependency Injection Setup

  • Research the latest stable version for Hilt and add the dependency to your app.
  • Dagger/Hilt rely on Annotations to work, so you will need Kotlin Annotation Processor Tool a.k.a. kapt
  • Add a class to your app that extends Application. Annotate it with @HiltAndroidApp. Remember adding its name to the application tag in the Android Manifest.
  • Create a /di package to add the files related to dependency injection
  • Create an object AppModule in /di. It will have the annotations @Module and @InstallIn(SingletonComponent::class)
  • Add the @Provides functions needed to provide the dependencies:
    • DataSourceAdapter/RemoteDataSource
    • PokemonService/PokemonServiceImpl
    • PokeApi/RetrofitClient.pokeService These dependencies need to be @Singleton, since they (particularly Retrofit) are quite resource-heavy and the app just needs one instantiation of each.

If the dependencies have their own dependencies (e.g. Retrofit client needs an okHttp client, a Converter Factory and, in our case, a LoggerInterception) add similar @Provides functions for those. See Additional Resources for examples.

Actual Injection

  • Once all the dependencies have their own @Provides function in the Module, start adding them to whichever classes need them using the @Inject annotation.
  • Bear in mind that hilt works differently for ViewModels. You'll need @HiltViewModel for the actual ViewModel, and hiltViewModel() for instantiating it. See Additional Resources for examples.

When everything is done and running, check the following:

  • No classes in /ui package have an import from /data
  • No classes in /domain package have an import from /ui nor from /data
  • No classes in /data have a dependency from /ui

Tips & Advice.

  • When creating the @Provides functions in the AppModule for a dependency that has its own dependencies, start with the smallest bit (the ones that need no other dependency to be instantiated)

Research terms.

  • Dependency Injection
  • Dagger Hilt
  • kapt
  • SingletonComponent
  • ActivityScoped
  • AndroidEntryPoint
  • Annotation: @Module, @installIn , @EntryPoint, @Inject

Additional resources.

@Provides function example If you have a dependency class ClassA that implements InterfaceA, its @Provides function would look like this

@Provides
fun provideInterfaceA(): InterfaceA{
    return ClassA()
}

If you have a dependency class ClassB that implements InterfaceB, and it has a depenedcy on ClassA, its @Provides function would look like this

@Provides
fun provideInterfaceB(dependencyA: InterfaceA): InterfaceB{
    return ClassB(dependencyA)
}

ClassC needs a dependency of type InterfaceB to doSomething():

class ClassC @Inject constructor (private val dependencyB: InterfaceB){
    // some code
    dependencyB.doSomething()
}

On your @Composable screen, declare your ViewModel as a parameter with a default value like this.

@Composable
fun MyScreen( // some parameters list
    viewModel: MyScreenViewModel = hiltViewModel()
){
    // your composable code
    viewModel.doScreenStuff()
}