头图

This article is the first article about Hilt MAD Skills series In this article, we will discuss the importance of dependency injection (DI) to applications and the Android DI solution recommended by Jetpack-Hilt.

If you prefer a video about this content, you can click here view.

In Android applications, you can lay the foundation for a good application architecture by following the principle of dependency injection. This helps reuse code, is easy to refactor, and easy to test! For more about the benefits of DI, please refer to: Dependency Injection in Android .

When creating an instance of a class in a project, you can manually process the dependency graph by providing and passing the required dependencies.

However, manual execution each time will increase the template code and is prone to errors. Taking a ViewModel in the iosched project (Google I/O open source application) as an example, can you imagine how much code is required to FeedViewModel

class FeedViewModel(
    private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
    loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
    private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
    getTimeZoneUseCase: GetTimeZoneUseCase,
    getConferenceStateUseCase: GetConferenceStateUseCase,
    private val timeProvider: TimeProvider,
    private val analyticsHelper: AnalyticsHelper,
    private val signInViewModelDelegate: SignInViewModelDelegate,
    themedActivityDelegate: ThemedActivityDelegate,
    private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
    FeedEventListener,
    ThemedActivityDelegate by themedActivityDelegate,
    SignInViewModelDelegate by signInViewModelDelegate {
    /* ... */
}

This is complicated and mechanized, and it is easy to mistake the dependencies. Dependency injection libraries allow us to take advantage of DI without having to provide dependencies manually, because the library will help you generate all the code you need. This is where Hilt comes into play.

Hilt

Hilt is a dependency injection library developed by Google. It helps you make full use of DI best practices in your application by handling complex dependencies and generating template code that you would otherwise need to write manually.

Hilt uses annotations to help you generate code at compile time to ensure runtime performance. the capabilities of the JVM DI library Dagger 1616d45ac1b7a9, and Hilt is built on Dagger.

Hilt is a DI solution for Android applications recommended by Jetpack. It comes with tools and supports other Jetpack libraries.

Quick start

All applications that use Hilt must include @HiltAndroidApp , which will trigger Hilt code generation at compile time. In order for Hilt to inject dependencies into the Activity, the Activity needs to use the @AndroidEntryPoint annotation.

@HiltAndroidApp
class MusicApp : Application()

@AndroidEntryPoint
class PlayActivity : AppCompatActivity() { /* ... */ }

When injecting a dependency, you need to add the @Inject annotation to the variable you want to inject. super.onCreate is called, all Hilt injected variables will be available.

@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {

  @Inject lateinit var player: MusicPlayer

  override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(bundle)
    player.play("YHLQMDLG")
  }
}

In this case, we have to PlayActivity injected into MusicPlayer , but Hilt is how to know how to provide MusicPlayer instance of a type it? Need extra work! We also need to tell Hilt what to do, and of course use annotations!

@Inject annotation to the construction method of the class to tell Hilt how to create an instance of this class.

class MusicPlayer @Inject constructor() {
  fun play(id: String) { ... }
}

This is all you need to inject dependencies into the Activity! very simple! We start with a simple example, because MusicPlayer does not depend on any other types. But if we pass other dependencies as parameters, Hilt will process and satisfy these dependencies when providing an instance of MusicPlayer.

In fact, this is a very simple and elementary example. But if you have to do our above work manually, what will you do?

manually implement

When manually executing DI, you need a dependency container, which is responsible for providing instances of the type and managing the life cycle of these instances. Simply put, these are what Hilt does behind the scenes.

When we add the @AndroidEntryPoint annotation to the Activity, Hilt will automatically create a dependency container, manage and associate it to PlayActivity . Here we manually implement the PlayActivityContainer container. By MusicPlayer add on @Inject notes, equivalent to tell the container how to provide MusicPlayer instance.

// PlayActivity 已被添加 @AndroidEntryPoint 注解
class PlayActivityContainer {

  // MusicPlayer 已被添加 @Inject 注解
  fun provideMusicPlayer() = MusicPlayer()

}

In Activity, we need to create a container instance and use it to assign values to Activity's dependencies. For Hilt, adding the @AndroidEntryPoint annotation to the Activity also completes the creation of the container instance.

class PlayActivity : AppCompatActivity() {

  private lateinit var player: MusicPlayer

  // 在 Activity 上添加 @AndroidEntryPoint 注解时由 Hilt 创建
  private lateinit var container: PlayActivityContainer


  override fun onCreate(savedInstanceState: Bundle) {

    // @AndroidEntryPoint 同样为您创建并填充字段
    container = PlayActivityContainer()
    player = container.provideMusicPlayer()

    super.onCreate(bundle)
    player.play("YHLQMDLG")
  }
}

comment review

So far, we have seen that when the @Inject annotation is added to the constructor of the class, it will tell Hilt how to provide an instance of the class. When the variable is @Inject , and the class to which the variable belongs is @AndroidEntryPoint , Hilt will inject an instance of the corresponding type into the class.

@AndroidEntryPoint annotations can be added to most Android framework classes, not just Activity. It will create an instance of the dependency container for the annotated class, and fill all the variables annotated @Inject

Add the @HiltAndroidApp Application class. In addition to triggering the Hilt code generation, a dependency container associated with the Application is also created.

Hilt module

Now that we have understood the Hilt basics, let's increase the complexity of the example together. Now, in the constructor of MusicPlayer MusicDatabase .

class MusicPlayer @Inject constructor(
  private val db: MusicDatabase
) {
  fun play(id: String) { ... }
}

Therefore, we need to tell Hilt how to provide MusicDatabase instance of 0616d45ac1bab7. When the type is an interface, or you cannot add @Inject constructor, for example, the class comes from a library that you cannot modify.

Suppose we use Room as a persistent storage library in our application PlayActivityContainer back to the scenario where we manually implement 0616d45ac1baf6, when we provide MusicDatabase through Room, this will be an abstract class, and we hope to execute some code when providing dependencies. Next, when providing MusicPlayer , we need to call the method MusicDatabase

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer() = MusicPlayer(
    provideMusicDatabase()
  )
}

In Hilt, we don't need to worry about transitive dependencies, because it will automatically associate all dependencies that need to be transitive. However, we need to let Hilt know how to provide an instance of type MusicDatabase For this, we use the Hilt module.

The Hilt module is a class annotated @Module In this class, we can implement functions to tell Hilt how to provide instances of the exact type. This type of information known to Hilt is also called binding in the industry.

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

  @Provides
  fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
    return Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()
  }
}

Add the @Provides annotation to this function to tell Hilt how to provide an instance of type MusicDatabase The function body contains the code block that Hilt needs to execute, which is completely consistent with our manual implementation.

The return type MusicDatabase tells Hilt what type this function provides. The parameters of the function tell Hilt the dependencies required for that type. In this case, ApplicationContext already available in Hilt. This code tells Hilt how to provide MusicDatabase instance of a type, in other words, we've got a MusicDatabase of binding .

The Hilt module also needs to add the @InstallIn annotation to indicate which dependency containers or components the information is available in. But what is a component? Let's introduce more details.

Hilt component

A component is a class generated by Hilt and is responsible for providing an instance of the type, just like the container we implement manually. At compile time, Hilt traverses the dependency graph and generates code to provide all types and carry their transitive dependencies.

△ 组件是一个 Hilt 生成的类,负责提供类型的实例

△ Component is a class generated by Hilt, responsible for providing instances of types

Hilt generates components (or dependency containers) for most Android framework classes. The associated information (or binding) of each component is passed down through the component hierarchy.

△ Hilt 的组件层次结构

△ Hilt's component hierarchy

If MusicDatabase binding in SingletonComponent (corresponding to Application class) is available, then the binding can also be used in other components.

When you add the @AndroidEntryPoint annotation to the Android framework class, Hilt will automatically generate the component during the compile time, and complete the creation, management and association of the component to the corresponding class.

@InstallIn annotation of the module is used to control the available locations of these bindings and which other bindings they can use.

limited scope

PlayActivityContainer back to the code for manually creating 0616d45ac1bdd5, are you aware of a problem? Every time the MusicDatabase dependency is needed, we will create a different instance.

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer() = MusicPlayer(
    provideMusicDatabase()
  )
}

This is not what we want, because we may want to reuse the same MusicDatabase instance throughout the application. We can share the same instance by holding a variable instead of a function.

class PlayActivityContainer {

  val musicDatabase: MusicDatabase =
    Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()

  fun provideMusicPlayer() = MusicPlayer(musicDatabase)
}

Basically we will MusicDatabase the scope of the 0616d45ac1be4d type to this container, because we will always provide the same instance as a dependency. How to achieve this through Hilt? Well, no doubt, use another annotation!

With the addition of the @Provides annotation, we can tell the Hilt component to always share the same instance of the type @Singleton

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

  @Singleton  
  @Provides
  fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
    return Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()
  }
}

@Singleton is a scope annotation. Each Hilt component has a scope annotation associated with it.

△ 不同 Hilt 组件的作用域注解

△ Scope annotation of different Hilt components

If you want to limit the scope of a type to ActivityComponent , you need to use the ActivityScoped annotation. These annotations can not only be used in the module, but also can be added to the class, provided that the construction method of the class has been added with the @Inject annotation.

bind

There are two types of bindings:

Jetpack extension

Hilt can be integrated with the most popular Jetpack libraries: ViewModel, Navigation, Compose and WorkManager.

In addition to ViewModel, each integration requires different libraries to be added to the project. For more information, please refer to: Hilt and Jetpack integrated . Do you remember we saw in the beginning of the article iosched in FeedViewModel code? Do you want to see the effect after using Hilt support?

@HiltViewModel
class FeedViewModel @Inject constructor(
    private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
    loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
    private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
    getTimeZoneUseCase: GetTimeZoneUseCase,
    getConferenceStateUseCase: GetConferenceStateUseCase,
    private val timeProvider: TimeProvider,
    private val analyticsHelper: AnalyticsHelper,
    private val signInViewModelDelegate: SignInViewModelDelegate,
    themedActivityDelegate: ThemedActivityDelegate,
    private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
    FeedEventListener,
    ThemedActivityDelegate by themedActivityDelegate,
    SignInViewModelDelegate by signInViewModelDelegate {
    /* ... */
}

In order to let Hilt know how to provide an instance of the ViewModel, we not only need to add the @Inject annotation to the constructor, we also need to add the @HiltViewModel annotation to this class.

That's it, Hilt will help you create a ViewModel provider, so you don't need to deal with it manually.

learn more

Hilt is built on Dagger, another popular dependency injection library! In the next article, Dagger will be mentioned frequently! If you are using Dagger, Dagger can be used with Hilt, please check our previous article " from Dagger to Hilt can bring benefits ". For more information about Hilt, you can refer to the following resources:

The above is the entire content of this article. We will launch more MAD Skills , so stay tuned for subsequent updates.

You are welcome to click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!


Android开发者
404 声望2k 粉丝

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