DI

依赖注入进化过程

graph LR
HardCode(HardCodeDependency)-->|进化|ServiceLocator
HardCode-->|进化|ManualDI-->|进化|Dagger

subgraph AutomatedDI
Dagger-->Hilt
end

Dependency injection in Android

Dependency injection (DI) is a technique widely used in programming and well suited to Android development. By following the principles of DI, you lay the groundwork for good app architecture.

Implementing dependency injection provides you with the following advantages:

  • Reusability of classes and decoupling of dependencies: It’s easier to swap out implementations of a dependency. Code reuse is improved because of inversion of control, and classes no longer control how their dependencies are created, but instead work with any configuration.通过替换被依赖者的具体实现,提升依赖者的复用性
  • Ease of refactoring,The dependencies become a verifiable part of the API surface, so they can be checked at object-creation time or at compile time rather than being hidden as implementation details.
  • Ease of testing,A class doesn’t manage its dependencies, so when you’re testing it, you can pass in different implementations to test all of your different cases.方便测试时进行mock替换被依赖者,完成不同场景的测试用例覆盖
  • 对象的构造和对象的使用分离,上层的使用者不用关心在哪里以及如何获取下层实例,直接使用构造方法传入的实例即可
  • 依赖者内部自行构造被依赖者,造成两者耦合,无法替换新的被依赖者实现

What is dependency injection?

Classes often require references to other classes. For example, a Car class might need a reference to an Engine class. These required classes are called dependencies, and in this example the Car class is dependent on having an instance of the Engine class to run.

There are three ways for a class to get an object it needs:

  1. The class constructs the dependency it needs. In the example above, Car would create and initialize its own instance of Engine.
  2. Grab it from somewhere else. Some Android APIs, such as Context getters and getSystemService(), work this way.
  3. Have it supplied as a parameter. The app can provide these dependencies when the class is constructed or pass them in to the functions that need each dependency. In the example above, the Car constructor would receive Engine as a parameter.

The third option is dependency injection! With this approach you take the dependencies of a class and provide them rather than having the class instance obtain them itself.

Ways to do dependency injection

There are two major ways to do dependency injection in Android:

  • Constructor Injection. This is the way described above. You pass the dependencies of a class to its constructor.
  • Field Injection (or Setter Injection). Certain Android framework classes such as activities and fragments are instantiated by the system, so constructor injection is not possible. With field injection, dependencies are instantiated after the class is created. The code would look like this:

Manual dependency injection

Manual dependency injection also presents several problems:

  • For big apps, taking all the dependencies and connecting them correctly can require a large amount of boilerplate code. In a multi-layered architecture, in order to create an object for a top layer, you have to provide all the dependencies of the layers below it. As a concrete example, to build a real car you might need an engine, a transmission, a chassis, and other parts; and an engine in turn needs cylinders and spark plugs.
  • When you’re not able to construct dependencies before passing them in — for example when using lazy initializations or scoping objects to flows of your app — you need to write and maintain a custom container (or graph of dependencies) that manages the lifetimes of your dependencies in memory.

Automated dependency injection

There are libraries that solve this problem by automating the process of creating and providing dependencies. They fit into two categories:

  • Reflection-based solutions that connect dependencies at runtime.
  • Static solutions that generate the code to connect dependencies at compile time.

Dagger is a popular dependency injection library for Java, Kotlin, and Android that is maintained by Google. Dagger facilitates using DI in your app by creating and managing the graph of dependencies for you. It provides fully static and compile-time dependencies addressing many of the development and performance issues of reflection-based solutions such as Guice.

Alternatives to dependency injection:ServiceLocator

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

The service locator pattern is different from dependency injection in the way the elements are consumed. With the service locator pattern, classes have control and ask for objects to be injected; with dependency injection, the app has control and proactively injects the required objects.

Compared to dependency injection:

  • The collection of dependencies required by a service locator makes code harder to test because all the tests have to interact with the same global service locator.
  • Dependencies are encoded in the class implementation, not in the API surface. As a result, it’s harder to know what a class needs from the outside. As a result, changes to Car or the dependencies available in the service locator might result in runtime or test failures by causing references to fail.
  • Managing lifetimes of objects is more difficult if you want to scope to anything other than the lifetime of the entire app.

Use Hilt in your Android app

Hilt is Jetpack’s recommended library for dependency injection in Android. Hilt defines a standard way to do DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically for you.

Hilt is built on top of the popular DI library Dagger to benefit from the compile time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.

To learn more about Hilt see Dependency Injection with Hilt.

总结

  1. 架构中上层需要通过构造方法传入下层的依赖
  2. 上述依赖的对象要通过依赖注入框架注入

依赖注入框架

graph TB
subgraph hilt

subgraph dagger

javax.inject

end
end

Dagger

https://developer.android.com/training/dependency-injection/dagger-basics

graph TB
Providers1("@Provides1")-->|dependency|Providers2("@Provides2")
Providers1-->|dependency|Inject1("@Inject1")
Providers2-->|dependency|Inject2("@Inject2")
Providers2-->|dependency|Inject3("@Inject3")
Inject3-->|dependency|Provides3("@Provides3")

@Inject

constructor

Add an @Inject annotation to the UserRepository constructor so Dagger knows how to create a UserRepository:

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

In the above snippet of code, you’re telling Dagger:

  1. How to create a UserRepository instance with the @Inject annotated constructor.
  2. What its dependencies are: UserLocalDataSource and UserRemoteDataSource.

Now Dagger knows how to create an instance of UserRepository, but it doesn’t know how to create its dependencies. If you annotate the other classes too, Dagger knows how to create them:

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

field

Instead of creating the dependencies an activity requires in the onCreate() method, you want Dagger to populate those dependencies for you. For field injection, you instead apply the @Inject annotation to the fields that you want to get from the Dagger graph.

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

,you need to tell Dagger about an object (LoginActivity in this case) that requires a dependency to be injected. For that, you expose a function that takes as a parameter the object that requests injection.

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}
class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

@Component

@Component tells Dagger to generate a container with all the dependencies required to satisfy the types it exposes. This is called a Dagger component; it contains a graph that consists of the objects that Dagger knows how to provide and their respective dependencies.

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}
// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Scoping

To have a unique instance of a UserRepository when you ask for the repository in ApplicationGraph, use the same scope annotation for the @Component interface and UserRepository. You can use the @Singleton annotation that already comes with the javax.inject package that Dagger uses:

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Alternatively, you can create and use a custom scope annotation. You can create a scope annotation as follows:

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Then, you can use it as before:

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

In both cases, the object is provided with the same scope used to annotate the @Component interface. Thus, every time you call applicationGraph.repository(), you get the same instance of UserRepository.

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

@Module

Apart from the @Inject annotation, there’s another way to tell Dagger how to provide an instance of a class: the information inside Dagger modules. A Dagger module is a class that is annotated with @Module. There, you can define dependencies with the @Provides annotation.

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

You can use the @Provides annotation in Dagger modules to tell Dagger how to provide classes that your project doesn’t own (e.g. an instance of Retrofit).

@Provides

@Provides, the most common construct for configuring a binding, serves three functions:

  1. Declare which type (possibly qualified) is being provided — this is the return type
  2. Declare dependencies — these are the method parameters
  3. Provide an implementation for exactly how the instance is provided — this is the method body

@Binds

Annotates abstract methods of a Module that delegate bindings. For example, to bind Random to SecureRandom a module could declare the following: @Binds abstract Random bindRandom(SecureRandom secureRandom);

@Binds methods are a drop-in replacement for Provides methods that simply return an injected parameter. Prefer @Binds because the generated implementation is likely to be more efficient.

A @Binds method:

  • Must be abstract.
  • May be scoped.
  • May be qualified.
  • Must have a single parameter whose type is assignable to the return type. The return type declares the bound type (just as it would for a @Provides method) and the parameter is the type to which it is bound.

@Subcomponent

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

You also must define a subcomponent factory inside LoginComponent so that ApplicationComponent knows how to create instances of LoginComponent.

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

To tell Dagger that LoginComponent is a subcomponent of ApplicationComponent, you have to indicate it by:

  1. Creating a new Dagger module (e.g. SubcomponentsModule) passing the subcomponent’s class to the subcomponents attribute of the annotation.
// The "subcomponents" attribute in the @Module annotation tells Dagger what
// Subcomponents are children of the Component this module is included in.
@Module(subcomponents = LoginComponent::class)
class SubcomponentsModule {}
  1. Adding the new module (i.e. SubcomponentsModule) to ApplicationComponent:
// Including SubcomponentsModule, tell ApplicationComponent that
// LoginComponent is its subcomponent.
@Singleton
@Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
}

Consumers of ApplicationComponent need to know how to create instances of LoginComponent. The parent component must add a method in its interface to let consumers create instances of the subcomponent out of an instance of the parent component:

  1. Expose the factory that creates instances of LoginComponentin the interface:
@Singleton
@Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
// This function exposes the LoginComponent Factory out of the graph so consumers
// can use it to obtain new instances of LoginComponent
fun loginComponent(): LoginComponent.Factory
}

Assigning scopes to subcomponents

What’s the lifecycle of LoginComponent? One of the reasons why you needed LoginComponent is because you needed to share the same instance of the LoginViewModel between Login-related fragments. But also, you want different instances of LoginViewModel whenever there’s a new login flow. LoginActivity is the right lifetime for LoginComponent: for every new activity, you need a new instance of LoginComponent and fragments that can use that instance of LoginComponent.

Because LoginComponent is attached to the LoginActivity lifecycle, you have to keep a reference to the component in the activity in the same way you kept the reference to the applicationComponent in the application class. That way, fragments can access it.

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Notice that the variable loginComponent is not annotated with @Inject because you’re not expecting that variable to be provided by Dagger.

You can use the ApplicationComponent to get a reference to LoginComponent and then inject LoginActivity as follows:

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Creation of the login graph using the application graph
        loginComponent = (applicationContext as MyDaggerApplication)
                            .appComponent.loginComponent().create()

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

LoginComponent is created in the activity’s onCreate() method, and it’ll get implicitly destroyed when the activity gets destroyed.

The LoginComponent must always provide the same instance of LoginViewModel each time it’s requested. You can ensure this by creating a custom annotation scope and annotating both LoginComponent and LoginViewModel with it. Note that you cannot use the @Singleton annotation because it’s already been used by the parent component and that’d make the object an application singleton (unique instance for the whole app). You need to create a different annotation scope.

Best practices when building a Dagger graph

When building the Dagger graph for your application:

  • When you create a component, you should consider what element is responsible for the lifetime of that component. In this case, the application class was in charge of ApplicationComponent and LoginActivity in charge of LoginComponent.
  • Use scoping only when it makes sense. Overusing scoping can have a negative effect on your app’s runtime performance: the object is in memory as long as the component is in memory and getting a scoped object is more expensive. When Dagger provides the object, it uses DoubleCheck locking instead of a factory-type provider.

Hilt

https://developer.android.com/training/dependency-injection/hilt-android

EntryPoint

@HiltAndroidApp

@HiltAndroidApp triggers Hilt’s code generation, including a base class for your application that serves as the application-level dependency container.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

@AndroidEntryPoint generates an individual Hilt component for each Android class in your project. These components can receive dependencies from their respective parent classes as described in Component hierarchy.

To obtain dependencies from a component, use the @Inject annotation to perform field injection:

@AndroidEntryPoint

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Define Hilt bindings

To perform field injection, Hilt needs to know how to provide instances of the necessary dependencies from the corresponding component. A binding contains the information necessary to provide instances of a type as a dependency.

One way to provide binding information to Hilt is constructor injection. Use the @Inject annotation on the constructor of a class to tell Hilt how to provide instances of that class:

@Inject constructor

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

The parameters of an annotated constructor of a class are the dependencies of that class. In the example, AnalyticsAdapter has AnalyticsService as a dependency. Therefore, Hilt must also know how to provide instances of AnalyticsService.

Note: At build time, Hilt generates Dagger components for Android classes. Then, Dagger walks through your code and performs the following steps:

  • Builds and validates dependency graphs, ensuring that there are no unsatisfied dependencies and no dependency cycles.
  • Generates the classes that it uses at runtime to create the actual objects and their dependencies.

Hilt modules

Sometimes a type cannot be constructor-injected. This can happen for multiple reasons. For example, you cannot constructor-inject an interface. You also cannot constructor-inject a type that you do not own, such as a class from an external library. In these cases, you can provide Hilt with binding information by using Hilt modules.

A Hilt module is a class that is annotated with @Module. Like a Dagger module, it informs Hilt how to provide instances of certain types. Unlike Dagger modules, you must annotate Hilt modules with @InstallIn to tell Hilt which Android class each module will be used or installed in.

@InstallIn

The Hilt module AnalyticsModule is annotated with @InstallIn(ActivityComponent::class) because you want Hilt to inject that dependency into ExampleActivity. This annotation means that all of the dependencies in AnalyticsModule are available in all of the app’s activities.

@Binds

The @Binds annotation tells Hilt which implementation to use when it needs to provide an instance of an interface.

The annotated function provides the following information to Hilt:

  • The function return type tells Hilt what interface the function provides instances of.
  • The function parameter tells Hilt which implementation to provide.
interface AnalyticsService {
  fun analyticsMethods()
}

// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}
@Provides

The annotated function supplies the following information to Hilt:

  • The function return type tells Hilt what type the function provides instances of.
  • The function parameters tell Hilt the dependencies of the corresponding type.
  • The function body tells Hilt how to provide an instance of the corresponding type. Hilt executes the function body every time it needs to provide an instance of that type.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

In cases where you need Hilt to provide different implementations of the same type as dependencies, you must provide Hilt with multiple bindings. You can define multiple bindings for the same type with qualifiers.

A qualifier is an annotation that you use to identify a specific binding for a type when that type has multiple bindings defined.

Consider the example. If you need to intercept calls to AnalyticsService, you could use an OkHttpClient object with an interceptor. For other services, you might need to intercept calls in a different way. In that case, you need to tell Hilt how to provide two different implementations of OkHttpClient.

First, define the qualifiers that you will use to annotate the @Binds or @Provides methods:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

Then, Hilt needs to know how to provide an instance of the type that corresponds with each qualifier. In this case, you could use a Hilt module with @Provides. Both methods have the same return type, but the qualifiers label them as two different bindings:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

You can inject the specific type that you need by annotating the field or parameter with the corresponding qualifier

As a best practice, if you add a qualifier to a type, add qualifiers to all the possible ways to provide that dependency. Leaving the base or common implementation without a qualifier is error-prone and could result in Hilt injecting the wrong dependency.

参考

https://developer.android.com/training/dependency-injection

Scoping in Android and Hilt

https://docs.oracle.com/javaee/6/api/javax/inject/package-summary.html

Dependency injection is based on the Inversion of Control principle in which generic code controls the execution of specific code.

Note: By default, bindings in Hilt are unscoped. They are not part of any component and they can be accessed throughout the entire project. A different instance of that type will be provided every time it is requested. When you scope a binding to a component, it limits where that binding can be used and which dependencies the type can have.