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.
△ 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'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.
△ 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:
- scope binding : Bindings without scope annotations, such as
MusicPlayer
, if they are not loaded into the module, all components can use these bindings. - limited scope binding : Binding with scope annotation added, such as
MusicDatabase
, and unqualified scope binding loaded into the module. Only corresponding components and components below the component hierarchy can use these 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:
- Dagger basics
- migrate to Hilt guide
- Hilt and Dagger annotation difference and usage memorandum
- Use Hilt to implement dependency injection
- Codelab: Use Hilt
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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。