Hilt is a new dependency injection code base developed based on Dagger , which simplifies the way to call Dagger in Android applications. This article shows you its core functions through short code snippets to help developers get started quickly with Hilt.
Configure Hilt
If you need to configure Hilt in the application, please refer to Gradle Build Setup .
After installing all the dependencies and plug-ins, you only need to add the @HiltAndroidApp annotation before your Application class to start using Hilt, no other operations are required.
@HiltAndroidApp
class App : Application()
defines and injects dependencies
When you write code that uses dependency injection, there are two important points to consider:
- The classes you need to inject dependencies;
- Classes that can be injected as dependencies.
The above two points are not mutually exclusive, and in many cases, your class can not only inject dependencies but also contain dependencies.
makes dependencies injectable
If you need to make a class in Hilt injectable, you need to tell Hilt how to create an instance of that class. This process is called bindings.
There are three ways to define bindings in Hilt:
- Add
@Inject
annotation to the constructor; @Binds
annotations on the module;@Provides
annotations on the module.
@Inject
annotation on the constructor
The constructor of any class can be @Inject
, so that this class can be injected as a dependency in the entire project.
class OatMilk @Inject constructor() {
...
}
⮕ Use modules
The other two ways to turn classes into injectable in Hilt are to use modules.
Hilt module is like a "recipe", it can tell Hilt how to create instances of classes that do not have a constructor, such as interfaces or system services.
In addition, in your test, any module can be replaced by other modules. This facilitates the use of mocks to replace interface implementations.
The module is installed in the specific Hilt component through the @InstallIn annotation. I will introduce this part in detail later.
Option 1: Use @Binds
to create a bond for the interface
If you want to use OatMilk
Milk
, you can create an abstract method in the module, and then add @Binds
annotations to the method. Note that OatMilk itself must be injectable, just add the @Inject
annotation to the OatMilk constructor.
interface Milk { ... }
class OatMilk @Inject constructor(): Milk {
...
}
@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
@Binds
abstract fun bindMilk(oatMilk: OatMilk): Milk
}
Option 2: Use @Provides to create a factory function
When the instance cannot be created directly, you can create a provider. A provider is a factory function that can return an object instance.
A typical example is system services, such as ConnectivityManager
, their instances need to be returned through the Context object.
@Module
@InstallIn(ApplicationComponent::class)
object ConnectivityManagerModule {
@Provides
fun provideConnectivityManager(
@ApplicationContext context: Context
) = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
As long as the annotation @ApplicationContext
or @ActivityContext
, the Context
object is injectable by default.
injection dependency
When the dependency is injectable, you can use Hilt in two ways:
- As a parameter injection of the constructor;
- Inject as a field.
⮕ as a constructor parameter injection
interface Milk { ... }
interface Coffee { ... }
class Latte @Inject constructor(
private val Milk milk,
private val Coffee coffee
) {
...
}
If the constructor uses the annotation @Inject
, Hilt will inject all the parameters according to the binding you defined for the type.
⮕ as a field injection
interface Milk { ... }
interface Coffee { ... }
@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
@Inject lateinit var milk: Milk
@Inject lateinit var coffee: Coffee
...
}
If the class is the entry point, here specifically refers to the class that uses the @AndroidEntryPoint
annotation (detailed in later chapters), then all @Inject
annotation will be injected.
The fields annotated with @Inject
must be of public type. You can also add lateinit
to avoid field null values, because their initial value before injection is null
.
Please note that the scenario of injecting dependencies as a field is only suitable for the case where the class must contain a parameterless constructor, such as Activity
. In most scenarios, you should inject dependencies through the parameters of the constructor.
Other important concepts
entry point
Remember what I mentioned above, in many cases, your class will contain injected dependencies while being created through dependency injection. In some cases, your class may not be created through dependency injection, but dependencies will still be injected. A typical example is activity, which is created internally by the Android framework, not by Hilt.
These classes belong to the entry point of the Hilt dependency map, and Hilt needs to know that these classes contain the dependencies to be injected.
⮕ Android entry point
Most of the entry points are the so-called Android entry point :
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
If it is an Android entry point, please add @AndroidEntryPoint annotation.
@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
...
}
⮕ Other entry points
The Android entry point is sufficient for most applications, but if you use a library that does not contain Dagger or an Android component that is not yet supported in Hilt, then you may need to create your own entry point to manually access the Hilt dependency graph. For details, please see Convert any class to entry point .
ViewModel
ViewModel is a special case: because the framework creates them, it is neither directly instantiated nor an Android entry point. ViewModel needs to use special @HiltViewModel
annotation. When ViewModel
is byViewModels()
, this annotation enables Hilt to inject dependencies into ViewModel, similar to the principle of @Inject
interface Milk { ... }
interface Coffee { ... }
@HiltViewModel
class LatteViewModel @Inject constructor(
private val milk: Milk,
private val coffee: Coffee
) : ViewModel() {
...
}
@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
private val viewModel: LatteViewModel by viewModels()
...
}
If you need to access ViewModel
cached state, you can add @Assisted
annotations will SavedStateHandle as a constructor parameter injection.
@HiltViewModel
class LatteViewModel @Inject constructor(
@Assisted private val savedState: SavedStateHandle,
private val milk: Milk,
private val coffee: Coffee
) : ViewModel() {
...
}
To use @ViewModelInject
, you may need to add more dependencies. For more details, please refer to Hilt and Jetpack Integration Guide .
component
Each module is installed in Hilt component , designated @InstallIn(<component name>). Module components are mainly used to prevent accidental injection of dependencies into wrong locations. For example,
@InstallIn(ServiceComponent.class)
can prevent the binding and provider in the module modified by the annotation from being called by the activity.
In addition, the scope of binding will be limited to the entire module to which the component belongs. That is what we are going to talk about next...
scope
By default, bindings are not scoped. Just like the example above, it means that every time you inject Milk
, you can get a new OatMilk
instance. If you add the @ActivityScoped
annotation, then you will limit the scope of the binding to ActivityComponent
.
@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
@ActivityScoped
@Binds
abstract fun bindMilk(oatMilk: OatMilk): Milk
}
Now your module is scoped, Hilt only creates one OatMilk instance in each activity instance. In addition, the OatMilk instance will be bound to the life cycle of the activity-when the activity's onCreate() is called, it will be created, and when the activity's onDestroy() is called, it will be destroyed.
@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
@Inject lateinit var milk: Milk
@Inject lateinit var moreMilk: Milk //这里的实例和上面的相同
...
}
In this example, milk
and moreMilk
point to the same OatMilk
instance. However, if you have multiple LatteActivity
instances, they will contain their own OatMilk
instances.
Correspondingly, other dependencies that are injected into the activity have the same scope. So they will also refer to the same OatMilk
instance:
// Milk 实例的创建会在 Fridge 存在之前,因为它被绑定到了 activity 的生命周期中
class Fridge @Inject constructor(private val Milk milk) { ... }
@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
// 下面四项共享了同一个 Milk 实例
@Inject lateinit var milk: Milk
@Inject lateinit var moreMilk: Milk
@Inject lateinit var fridge: Fridge
@Inject lateinit var backupFridge: Fridge
...
}
The scope depends on the components installed by your module. For example, @ActivityScoped
only used for binding within the module installed by ActivityComponent
The scope also determines the life cycle of the injected instance: In this example, a single instance of Milk used Fridge
and LatteActivity
LatteActivity
of onCreate()
is called-and destroyed when onDestroy() is called . This also means that when the configuration changes, Milk will not "survive", because the activity's onDestroy() will be called when the configuration changes. You can avoid this problem by using a longer-lived scope, such as @ActivityRetainedScope
.
If you want to know the list of available scopes, related components, and the life cycle followed, see Hilt component .
Provider injection
Sometimes you want to be able to more directly control the creation of injected instances. For example, you may want to inject one instance or several instances of a certain type based on business logic. For such scenarios, you can use dagger.Provider :
class Spices @Inject constructor() { ... }
class Latte @Inject constructor(
private val spiceProvider: Provider<Spices>
) {
fun addSpices() {
val spices = spiceProvider.get()// 创建 Spices 的新实例
...
}
}
Provider injection can ignore specific dependency types and injection methods. Any content that can be injected can be encapsulated in Provider<...>
to use provider injection.
Dependency injection frameworks (like Dagger and Guice ) are usually used for large and complex projects. Hilt is easy to use and very simple to configure. At the same time, as an independent code package, it also takes into account the powerful features of Dagger that can be used by various types of applications, regardless of code size.
If you want to know more about Hilt, its working principle, and other useful features for you, please visit the official website, for more detailed introduction and reference document .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。