graph LR
HardCode(HardCodeDependency)-->|进化|ServiceLocator
HardCode-->|进化|ManualDI-->|进化|Dagger
subgraph AutomatedDI
Dagger-->Hilt
end
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:
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:
Car
would create and initialize its own instance of Engine
.Context
getters and getSystemService()
, work this way.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.
There are two major ways to do dependency injection in Android:
Manual dependency injection also presents several problems:
There are libraries that solve this problem by automating the process of creating and providing dependencies. They fit into two categories:
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.
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:
Car
or the dependencies available in the service locator might result in runtime or test failures by causing references to fail.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.
graph TB
subgraph hilt
subgraph dagger
javax.inject
end
end
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")
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:
UserRepository
instance with the @Inject
annotated constructor.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() { ... }
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
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 {
...
}
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)
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
, the most common construct for configuring a binding, serves three functions:
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:
abstract
.Provides
method) and the parameter is the type to which it is bound.// @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:
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 {}
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:
LoginComponent
in 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
}
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.
When building the Dagger graph for your application:
ApplicationComponent
and LoginActivity
in charge of LoginComponent
.DoubleCheck
locking instead of a factory-type provider.https://developer.android.com/training/dependency-injection/hilt-android
@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
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
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:
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:
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.
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.
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:
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
}
The annotated function supplies the following information to Hilt:
@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
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.