头图

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:

  1. The classes you need to inject dependencies;
  2. 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:

  1. Add @Inject annotation to the constructor;
  2. @Binds annotations on the module;
  3. @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:

  1. As a parameter injection of the constructor;
  2. 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 .


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。