With the release of the Android 11 system, the Jetpack family has introduced many new members, including Hilt, App Startup, Paging3, and more.
Regarding App Startup, you can refer to my last post
The subject of this post is Hilt.
Hilt is a powerful and easy-to-use dependency injection framework
Why use dependency injection?
The most well-known application for dependency injection is probably the Spring framework in Java. Spring was actually a framework for dealing with dependency injection at the beginning, and later it gradually became a comprehensive framework with more extensive functions.
So why should we use Dependency Injection? in one word: decoupling
Excessive coupling may be a serious hidden danger in your project, and it will make your project more and more difficult to maintain in the later stages.
In order to make it easier for everyone to understand, here I am going to talk about it through a specific example.
Let’s say we start a truck delivery company that currently has a truck that makes deliveries every day and makes money to keep the company running.

I received a delivery order today, and a customer entrusted our company to deliver two computers.

To accomplish this task, we can write the following code:
class Truck {
val computer1 = Computer()
val computer2 = Computer()
fun deliver() {
loadToTruck(computer1)
loadToTruck(computer2)
beginToDeliver()
}
}
Code language: JavaScript (javascript)
Here is a truck, and there is a deliver() function in the truck to perform the delivery task. In the deliver() function, we load the two computers into the truck and start delivering.
Does this way of writing get the job done? Of course, our task is to distribute two computers. Now that both computers are distributed, the task is of course completed.
But is there any problem with this way of writing? Yes, and seriously.
Where is the specific problem? The discerning friends should have seen that, we created two computer instances in the Truck class, and then delivered them. In other words, now our trucks must not only deliver goods, but also produce computers.
This is the problem caused by the over-coupling just mentioned. The truck and the computer are two originally unrelated things coupled together.
If you think that the current writing problem is not serious, the company received a new order the next day, asking us to deliver mobile phones, so this truck must be able to produce mobile phones. On the third day, I received another order for the delivery of vegetables and fruits, so the truck would also be able to farm. . .

In the end, you will find that this is not a truck, but a global commodity manufacturing center.
Now that we all realize the seriousness of the problem, let’s go back and reflect, where did our project start to deviate?
This is a structural design problem. Thinking about it carefully, the truck doesn’t really need to care about the specific goods being delivered, its job is just to deliver the goods. Therefore, you can understand that the truck is dependent on the goods. If the truck is given the goods, it will deliver the goods, and if the truck is not given the goods, it will stand by.
Then according to this statement, we can modify the code just now as follows:
class Truck {
lateinit var cargos: List<Cargo>
fun deliver() {
for (cargo in cargos) {
loadToTruck(cargo)
}
beginToDeliver()
}
}
Code language: PHP (php)
The trucks field is now added to the Truck class, which means that the truck is dependent on the cargo. After such a modification, our trucks no longer care about the manufacture of any goods, but rely on what goods to deliver, and only do what they are supposed to do.
This way of writing, we can call it: dependency injection
.
What is the role of a dependency injection framework?
At present, the Truck class has been designed reasonably, but then a new problem will arise. If our identities have now changed and become the owners of a computer company, how do I get a truck to deliver my computers for me?
This is not easy to do? Many people can naturally write the following code:
class ComputerCompany {
val computer1 = Computer()
val computer2 = Computer()
fun deliverByTruck() {
val truck = Truck()
truck.cargos = listOf(computer1, computer2)
truck.deliver()
}
}
Code language: JavaScript (javascript)
This code can also work normally, but this code also has serious problems.
Where is the problem? It is in the deliverByTruck()
function, in order to let the truck help us deliver, we made a truck
by ourselves. This is obviously unreasonable, the computer company should only be responsible for producing computers, it should not be producing trucks.
So it makes more sense for us to call the truck delivery company and have them send a spare truck over so we don’t have to build it ourselves. When the truck arrives, we put the computer on the truck and perform the delivery task.
Projects designed using this structure will have excellent scalability. If there is another vegetable and fruit company that needs to find a truck to deliver vegetables, we can use the same structure to complete the task
Note, here comes the point. Calling the truck company and having them schedule this part of the idle vehicle, we can do it by hand, or we can use some dependency injection framework to simplify the process.
Does Android development also need a dependency injection framework?
There are many people who hold the view that the dependency injection framework is mainly applied to. The more complex programs such as the server, and Android development usually does not use the dependency injection framework at all.
Think of dependency injection frameworks as a tool to help us simplify our code and optimize our projects, rather than an additional burden.
So, no matter the complexity of the program is high or low, since the dependency injection framework can help us simplify the code and optimize the project, then it can be used completely.
Speaking of optimization projects, you might think the example I just gave of making a computer with a truck is too funny. But believe it or not, in our actual development process, such examples are played out almost every day.
Think about it, in the code you usually write in Activity, have you ever created an instance that should not be created by Activity?
For example, we all use OkHttp for network requests. Have you ever created an instance of OkHttpClient in your Activity? If there is, then congratulations, you are equivalent to making the truck to produce the computer (Activity is the truck, OkHttpClient is the computer).
Of course, if it is just a relatively simple project, we can indeed create an instance of OkHttpClient in Activity. Irrespective of the degree of code coupling, it wouldn’t be too much of a problem if the truck were to actually make a computer, because it would work. At least temporarily.
The first time I clearly realized that I desperately needed a dependency injection framework was when I was building a project using the MVVM architecture.
There is a schematic diagram of the MVVM architecture on the official website of Android developers, as shown in the following figure

This is the Android application architecture that Google recommends us most now.
This architecture diagram tells us that a well-architected project should be divided into several layers.
The green part represents the UI control layer, which is the Activity and Fragment we usually write.
The blue part represents the ViewModel layer. The ViewModel is used to hold data related to UI elements and to communicate with the warehouse.
The orange part represents the warehouse layer. The job of the warehouse layer is to determine whether the data requested by the interface should be read from the database or obtained from the network, and return the data to the caller. In short, the job of the warehouse is to do a job of distribution and scheduling between local and network data.
In addition, all the arrows in the figure are one-way, for example, Activity points to ViewModel, indicating that Activity is dependent on ViewModel, but ViewModel cannot depend on Activity in turn. The same is true for other layers, an arrow represents a dependency.
In addition, dependencies cannot cross layers. For example, the UI control layer cannot have a dependency relationship with the warehouse layer, and the components of each layer can only interact with its adjacent layers.
The project designed using this architecture has a clear structure and clear layers, and it will definitely be a project with very high code quality.
But in the process of implementing according to this architecture diagram, I found a problem.
In the UI control layer, Activity is one of the four components, and its instance creation does not need us to worry about it.
In the ViewModel layer, Google provides a special API in Jetpack to obtain ViewModel instances, so we don’t need to worry about its instance creation.
But when it comes to the warehouse layer, an embarrassing thing occurs, who should be responsible for creating an instance of the warehouse? ViewModel? No, ViewModel just depends on the warehouse, it should not be responsible for creating an instance of the warehouse, and other different ViewModels may also depend on the same warehouse instance. Activity? This is even more ridiculous, because Activity and ViewModel usually have a one-to-one correspondence.
So in the end I found that no one should be responsible for creating instances of the repository. The easiest way is to set the repository as a singleton class, so that you don’t need to worry about instance creation.
However, after setting it as a singleton class, a new problem will arise, that is, the rule that dependencies cannot cross layers is broken. Because the warehouse has been set as a singleton class, it is naturally equivalent to everyone owning its dependencies. The UI control layer can bypass the ViewModel layer and communicate directly with the warehouse layer.
From the perspective of code design, this is a very difficult problem to solve. But if we use the dependency injection framework, we can solve this problem very flexibly.
As can be seen from the schematic diagram just now, the dependency injection framework is to help us call and arrange an idle truck. I don’t care how this truck came, as long as you can help me deliver it.
Therefore, the ViewModel layer should not care about how the instance of the warehouse comes from. I only need to declare that the ViewModel needs to depend on the warehouse, and let the dependency injection framework help me to solve the rest.
Through such an analogy, do you have a deeper understanding of the dependency injection framework?
Android commonly used dependency injection framework
Next, let’s talk about the commonly used dependency injection frameworks for Android.
In the very early days, most Android developers were not aware of the use of dependency injection frameworks.
In 2012, the well-known Square company launched the open source dependency injection framework that is still very well-known: Dagger.
Square company has many very successful open source projects, OkHttp, Retrofit, LeakCanary, etc. are familiar to everyone, and almost all Android projects are used. But Dagger is not well-known, and there should be no projects still using it now, why?
This is an interesting story.
Although Dagger’s dependency injection concept is very advanced, there is a problem. It is implemented based on Java reflection, which leads to two potential hidden dangers.
First, we all know that reflection is time-consuming, so using this method will reduce the efficiency of the program. Of course, this is not a big problem, because reflection is used everywhere in today’s programs.
Second, the usage of dependency injection frameworks is generally very difficult, unless you are quite proficient in using it, it is difficult to write correctly the first time. The dependency injection function implemented based on reflection makes it impossible for us to know whether the usage of dependency injection is correct at compile time, and can only be judged by whether the program crashes at runtime. In this way, the efficiency of the test is very low, and it is easy to hide some bugs deeply.
Next comes the most interesting part. We all know that there are problems with the implementation of Dagger, so Dagger2 is naturally going to solve these problems. But Dagger2 is not developed by Square, but by Google.

This is very strange. Under normal circumstances, the 1st and 2nd versions of a library should be maintained by the same company or the same group of developers. How can Dagger1 and Dagger2 change so much? I don’t know why, but I noticed that the Dagger project currently maintained by Google is fork from Square’s Dagger project.
So I guess, Google fork a Dagger source code, and then modified it on this basis, and released the Dagger2 version. After seeing it, Square thought that Google’s version had done very well, and there was no need to redo it again, and there was no need to continue to maintain Dagger1, so it issued this statement:

So what is the difference between Dagger2 and Dagger1? The most important difference is that the implementation has completely changed. We have already known just now that Dagger1 is implemented based on Java reflection and listed some of its drawbacks. The Dagger2 developed by Google is implemented based on Java annotations, which solves all the drawbacks of reflection.
Through annotations, Dagger2 will automatically generate code for dependency injection at compile time, so it will not increase any running time. In addition, Dagger2 will check whether the developer’s dependency injection usage is correct at compile time. If it is not correct, the compilation will fail directly, so that the problem can be thrown as early as possible. That is to say, as long as your project compiles normally, it basically means that there is no problem with your dependency injection usage.
So has Google’s Dagger2 succeeded? It can be said that it was a great success.
According to Google’s official data, among the top 10,000 apps on Google Play, 74% of the apps use Dagger2.

Here I would like to mention that the technology stacks that overseas and domestic Android developers like to study are different. Overseas, no one researches such domestic-specific Android technologies as hotfixing or plug-in. Then you may want to ask, what advanced are overseas developers learning?
The answer is Dagger2.
Yes, Dagger2 is a very popular and widely recognized technology stack overseas. If you can use Dagger2 well, it basically means that you are a relatively high-level developer.
But what is interesting is that there are not many people in China who are willing to use Dagger2. I have also pushed several articles about Dagger2 before the official account, but from the feedback, I feel that this technology is always relatively small in China.
While Dagger2 is very popular overseas, it is known for its complexity, and if you don’t use it well, it can actually slow down your project. So there have always been voices saying that using Dagger2 will overdesign some simple projects.
According to a survey released by the Android team, 49% of Android developers want a simpler dependency injection solution in Jetpack.
So, Google released Hilt this year.
Do you think I’ve talked so much at length that I’m finally getting to the subject? Don’t think so, I think it is more important to understand the above comprehensive content than just grasp the usage of Hilt.
We all know that Dagger means a dagger, and dependency injection is like inserting a dagger directly into the place where it needs to be injected, hitting the key.
Hilt means the handle of the knife, it hides the sharpest part of the dagger, because if you don’t use the dagger well, you may accidentally injure yourself. Hilt provides you with a secure handle, ensuring you can use it safely and easily.
In fact, Hilt and Dagger2 are inextricably linked. Hilt is a dependency injection framework specially developed for Android that the Android team contacted the Dagger2 team and developed together. Compared with Dagger2, the most obvious features of Hilt are: 1. Simple. 2. Provides an Android-specific API.
So next, let us start to learn the specific usage of Hilt.
Introduce Hilt
Before starting to use Hilt, we need to introduce Hilt into your current project. This process is a little tedious, so please follow the steps in the article step by step.
The first step, we need to configure Hilt’s plugin path in the build.gradle file in the project root directory:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
Code language: JavaScript (javascript)
It can be seen that the latest version of Hilt’s plug-in is still in the alpha stage, but it doesn’t matter. I feel that it is quite stable after using it myself. I can upgrade it after the official version is released, and there will be no major changes in usage. .
Next, in the app/build.gradle file, import Hilt’s plugin and add Hilt’s dependency library:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
Code language: JavaScript (javascript)
The kotlin-kapt plugin is also introduced here because Hilt is implemented based on compile-time annotations, and the kotlin-kapt plugin must be added to enable the compile-time annotation function. If you are still developing projects in Java, you can not introduce this plugin, and change the kapt keyword used when adding annotation dependencies to annotationProcessor.
Finally, since Hilt will also use Java 8 features, we have to enable Java 8 features in the current project, edit the app/build.gradle file, and add the following:
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
Well, that’s all there is to configure. Now that you have successfully introduced Hilt into your project, let’s learn how to use it.
Simple usage of Hilt
Let’s start with the simplest functions first.
I believe everyone knows that there will be an Application in every Android program. This Application can be customized or not defined. If you do not define it, the system will use a default Application.
In Hilt, you must customize an Application, otherwise Hilt will not work properly.
Here we customize a MyApplication class, the code is as follows:
@HiltAndroidApp
class MyApplication : Application() {
}
Code language: CSS (css)
You must add a @HiltAndroidApp
annotation, which is a prerequisite for using Hilt.
Next register MyApplication into your AndroidManifest.xml file:
<application
android:name=".MyApplication"
...>
</application>
Code language: HTML, XML (xml)
In this way, the preparation work is completed, and the next work is to use Hilt to perform dependency injection according to your specific business logic.
Hilt
greatly simplifies the usage of Dagger2, so that we do not need to write the logic of the bridge layer through the @Component
annotation, but it also limits the injection function to only start from a few fixed Android entry points.
Hilt supports a total of 6 entry points, which are:
- Application
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
Among them, only the entry point of Application is declared using the@HiltAndroidApp
annotation, which we have just seen. All other entry points are declared with the@AndroidEntryPoint
annotation.
Take the most common Activity as an example. If I want to perform dependency injection in the Activity, I only need to declare the Activity like this:
@AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
Next let’s try to inject something into the Activity. Inject what? Remember the truck just now, let’s try to inject it into the Activity.
Define a Truck class, the code is as follows:
class Truck {
fun deliver() {
println("Truck is delivering cargo.")
}
}
Code language: JavaScript (javascript)
As you can see, the truck currently has a deliver()
method, indicating that it has a delivery function.
Then modify the code in the Activity as follows:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
truck.deliver()
}
}
Code language: CSS (css)
The code here might look a little weird at first, so let me explain.
First of all, lateinit
is a keyword in Kotlin and has nothing to do with Hilt. This keyword is used to delay initialization of variables, because Kotlin defaults to initializing a variable when it is declared, and here we don’t want to initialize it manually, so add lateinit
. If you are developing in Java, you can ignore this keyword.
Next, we declared an @Inject
annotation above the truck
field, indicating that I want to inject the truck field through Hilt. If I were to use an analogy, this would be roughly the process of a computer company calling a truck delivery company to arrange a truck. We can think of MainActivity as a computer company, it depends on a truck, but as to how the truck came about, the computer company doesn’t care. Hilt’s role here is similar to that of a truck delivery company, which is responsible for figuring out how to arrange vehicles and even has the obligation to build one.
In addition, the fields injected by Hilt cannot be declared private
, so everyone must pay attention here.
But the code doesn’t work right here, because Hilt doesn’t know how to deliver a truck. Therefore, we also need to modify the Truck class as follows:
class Truck @Inject constructor() {
fun deliver() {
println("Truck is delivering cargo.")
}
}
Code language: JavaScript (javascript)
Here we declare an @Inject
annotation on the constructor of the Truck class, which is actually telling Hilt that you can arrange a truck through this constructor.
Well, it’s that simple. Now you can run the program and you will see the following in Logcat:

It means that the truck is really delivering goods.
Did it feel amazing? We did not create an instance of Truck
in MainActivity
, but declared it with @Inject
, and it turned out that its deliver()
method could really be called.
This is the dependency injection function that Hilt provides us.
Dependency injection with parameters
It must be admitted that the example we just gave is indeed too simple, and its usefulness in real programming scenarios should be very limited, because this ideal situation cannot always be the case in real scenarios.
So let’s start to learn step by step how to use Hilt for dependency injection in various more complex scenarios.
First of all, a scenario that is easy to think of. If my constructor has parameters, how does Hilt perform dependency injection?
We modify the Truck class as follows:
class Truck @Inject constructor(val driver: Driver) { fun deliver() { println("Truck is delivering cargo. Driven by $driver") } }
It can be seen that a Driver parameter is now added to the constructor of the Truck
class, indicating that the truck depends on a driver. After all, the truck will not drive without a driver.
So the question is, since the truck is dependent on the driver, how does Hilt now perform dependency injection on the truck? After all, Hilt didn’t know where the driver came from.
This problem is actually not as difficult as imagined, because since the truck is dependent on the driver, if we want to perform dependency injection on the truck, we must first be able to perform dependency injection on the driver.
So you can declare the Driver class like this:
class Driver @Inject constructor() {
}
Code language: JavaScript (javascript)
Very simple, we declare a @Inject
annotation on the constructor of the Driver
class, so that the Driver
class becomes the dependency injection method of the no-argument constructor.
Then there is no need to modify any code, because since Hilt knows how to dependency inject Driver, it also knows how to dependency inject Truck.
To sum up, all other objects that the constructor of Truck depends on support dependency injection, and then Truck can be dependency injected.
Now re-run the program and print the log as follows:

It can be seen that the truck is now being driven by a driver whose ID number is de5edf5.
Dependency injection for interfaces
Solved the dependency injection of the constructor with parameters, let’s move on to a more complex scenario: how to perform dependency injection on the interface.
There is no doubt that the technology we currently have is unable to perform dependency injection on interfaces, and the reason is very simple. Interfaces do not have constructors.
But don’t worry, Hilt has fairly well-developed support for interface dependency injection, so you’ll be able to pick up the slack in no time.
We continue to learn through concrete examples.
Any truck needs an engine to drive normally, so here I define an Engine interface as follows:
interface Engine {
fun start()
fun shutdown()
}
Code language: PHP (php)
Very simple, there are two methods to be implemented in the interface, which are used to enable the engine and disable the engine.
Since there is an interface, there must be an implementation class. Here I define another GasEngine class and implement the Engine interface. The code is as follows:
class GasEngine() : Engine { override fun start() { println("Gas engine start.") } override fun shutdown() { println("Gas engine shutdown.") } }
As you can see, we have implemented the function of starting the engine and closing the engine in GasEngine
.
In addition, new energy vehicles are very popular now, and Tesla is almost everywhere. Therefore, in addition to the traditional gasoline engine, the car engine now has an electric engine. So here we define another ElectricEngine class and implement the Engine interface. The code is as follows:
class ElectricEngine() : Engine { override fun start() { println("Electric engine start.") } override fun shutdown() { println("Electric engine shutdown.") } }
Similarly, the function of starting the engine and closing the engine is also implemented in ElectricEngine
.
As I said just now, any truck needs an engine to run normally, that is to say, the truck is dependent on the engine. Now I want to inject the engine into the truck by means of dependency injection, so how do I need to write it?
Based on what you have just learned, the most intuitive way to write it is this:
class Truck @Inject constructor(val driver: Driver) {
@Inject
lateinit var engine: Engine
...
}
Code language: CSS (css)
We declare an engine field in Truck, which means that Truck depends on Engine. Then use the @Inject
annotation above the engine field to inject the field. Or you can declare the engine field in the constructor, so you don’t need to add the @Inject
annotation, the effect is the same.
If the Engine field is an ordinary class, it is of course no problem to use this way of writing. But the problem is that Engine is an interface, and Hilt must not know how to create an instance of this interface, so writing this way will definitely report an error.
Let’s take a look at how to solve this problem step by step.
First of all, the two implementation classes, GasEngine
and ElectricEngine
, just written, can be dependency injected because they both have constructors.
So modify the code in GasEngine and ElectricEngine respectively as follows:
class GasEngine @Inject constructor() : Engine {
...
}
class ElectricEngine @Inject constructor() : Engine {
...
}
Code language: CSS (css)
This is again the technique we just learned, declaring the @Inject
annotation on the constructors of these two classes respectively.
Next, we need to create a new abstract class. The class name can be anything, but it is best to be related to the business logic, so I suggest to name it EngineModule.kt
, as shown below:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
}
Code language: PHP (php)
Note here that we need to declare a @Module
annotation above EngineModule
, indicating that this is a module used to provide dependency injection instances.
If you have learned Dagger2 before, then it will be quite easy to understand this part, which is exactly the same as Dagger2.
And if you haven’t learned Dagger2 before, it doesn’t matter, follow the next steps to implement it step by step, and you will naturally understand its role.
In addition, you may notice that in addition to the @Module
annotation, an @InstallIn
annotation is also declared here, which is something that Dagger2 does not have. Regarding the role of the @InstallIn
annotation, I will use a separate topic to explain it later. For the time being, you only need to know that you must write it like this.
After defining the EngineModule, we need to provide the instances required by the Engine interface in this module. How to provide it? Very simple, the code looks like this:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
Code language: CSS (css)
Here are a few key points I will explain one by one.
- First of all we have to define an abstract function, why is it an abstract function? Because we don’t need to implement a specific function body.
- Second, it doesn’t matter what the function name of this abstract function is, you won’t call it, but a good name can help your reading and understanding.
- Third, the return value of the abstract function must be Engine, indicating that it is used to provide an instance to the interface of the Engine type. So what instance to provide it? The abstract function receives any parameters, and provides any instance to it. Since our truck is still relatively traditional and still uses a fuel engine, the
bindEngine()
function receives theGasEngine
parameter, that is, it provides an instance ofGasEngine
to the Engine interface.
Finally, add the @Bind
annotation above the abstract function so that Hilt can recognize it.
After a series of coding, we return to the Truck class. You will find that it makes sense for us to perform dependency injection into the engine field at this time, because with the help of the EngineModule
just defined, it is obvious that an instance of GasEngine
will be injected into the engine field.
Is this actually the case? Let’s just operate it, and modify the code in the Truck class as follows:
class Truck @Inject constructor(val driver: Driver) { @Inject lateinit var engine: Engine fun deliver() { engine.start() println("Truck is delivering cargo. Driven by $driver") engine.shutdown() } }
We start the vehicle engine before starting the delivery, and then finish the vehicle engine after the delivery is completed, very reasonable logic.
Now re-run the program, the console prints information as shown:

As we expected, the logs for fuel engine on and fuel engine off were printed before and after delivery, indicating that Hilt did inject an instance of GasEngine
into the engine field.
This also solves the problem of dependency injection to the interface.
Inject different instances of the same type
Friendly reminder, don’t forget that the ElectricEngine
we defined just now is not used yet.
Now that truck delivery companies have made a lot of money through deliveries and solved the problem of food and clothing, it’s time to consider environmental issues. It’s not environmentally friendly enough to use a gasoline engine to deliver goods. In order to save the planet, we decided to upgrade the truck.
However, at present, electric vehicles are not mature enough, and there are problems such as short cruising range and long charging time. How to do it? So we are going to take a compromise and temporarily use a hybrid engine for the transition.
In other words, a truck will have both a gasoline engine and an electric engine.
So the question is, we provide an instance for the Engine interface through the bindEngine() function in the EngineModule. This instance is either GasEngine or ElectricEngine. How can we provide two different instances for an interface at the same time?
As you may think, then I define two different functions to receive GasEngine and ElectricEngine parameters respectively. The code is as follows:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
Code language: CSS (css)
This way of writing seems reasonable, but if you compile it, you will find an error,this error reminds us that the Engine has been bound many times.
In fact, it makes sense to think about it. We provide two different functions in EngineModule, and their return values are both Engine. So when dependency injection is performed on the engine field in Truck, what is the instance provided by the bindGasEngine() function? Or use the instance provided by the bindElectricEngine()
function? Hilt couldn’t figure it out either.
Therefore, this problem requires additional technical means to solve: Qualifier annotation.
The role of the Qualifier annotation is to solve the problems we are currently encountering, injecting different instances into the same type of class or interface.
Here we define two annotations respectively, as follows:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
Code language: JavaScript (javascript)
One annotation is called BindGasEngine, and the other is called BindElectricEngine, so the roles of the two annotations are clearly distinguished.
In addition, the above annotation must be declared with @Qualifier, there is no doubt about this. As for the other @Retention, it is used to declare the scope of the annotation. Selecting AnnotationRetention.BINARY means that the annotation will be retained after compilation, but the annotation cannot be accessed through reflection. This should be the most reasonable annotation scope.
After defining the above two annotations, we go back to the EngineModule. Now you can add the two annotations you just defined above the bindGasEngine() and bindElectricEngine() functions, respectively, as follows:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine
@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
Code language: CSS (css)
In this way, we have classified two functions that provide instances for the Engine interface, one is assigned to the @BindGasEngine
annotation, and the other is assigned to the @BindElectricEngine
annotation.
But it’s not over yet, because after adding the Qualifier annotation, all the places where dependency injection is performed for the Engine
type also need to declare the annotation to clearly specify which type of instance you want to inject.
So we also need to modify the code in the Truck class as follows:
class Truck @Inject constructor(val driver: Driver) { @BindGasEngine @Inject lateinit var gasEngine: Engine @BindElectricEngine @Inject lateinit var electricEngine: Engine fun deliver() { gasEngine.start() electricEngine.start() println("Truck is delivering cargo. Driven by $driver") gasEngine.shutdown() electricEngine.shutdown() } }
Does this code seem easy to understand now?
We define two fields, gasEngine
and electricEngine
, both of which are of type Engine
. But above the gasEngine
, the @BindGasEngine
annotation is used so that Hilt will inject an instance of GasEngine
into it. Above the electricEngine, the @BindElectricEngine annotation is used so that Hilt will inject an instance of ElectricEngine into it.
Finally, in deliver()
, we start the fuel engine first, and then start the electric engine. After the delivery, we turn off the fuel engine first, and then turn off the electric engine.
What will the end result look like? Let’s run it, as shown in the figure below.

Great, everything worked as we expected.
This also solves the problem of injecting different instances of the same type.
Dependency injection for third-party classes
The truck example will come to an end for now, let’s look at some more practical examples.
As I said just now, if we want to use OkHttp
to initiate network requests in MainActivity
, we usually create an instance of OkHttpClient
. However, in principle, the instance of OkHttpClient
should not be created by Activity, so obviously, using dependency injection at this time is a very good solution. That is, let MainActivity depend on OkHttpClient.
But this will lead to a new problem. The OkHttpClient class is provided by the OkHttp library. We do not have the permission to write this class, so it is naturally impossible to add the @Inject annotation to the constructor of OkHttpClient. What about dependency injection on it?
At this time, we need to use the @Module annotation. Its solution is somewhat similar to providing dependency injection for interface types, but it is not exactly the same.
First define a class called NetworkModule, the code is as follows:
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
}
Code language: CSS (css)
Its initial declaration is very similar to the previous EngineModule
, except that it is not declared as an abstract class here, because we will not define abstract functions here.
Obviously, in the NetworkModule
, we want to provide an instance of the OkHttpClient
type, so we can write the following code:
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
}
Code language: CSS (css)
Similarly, the function name of provideOkHttpClient()
is arbitrarily defined, Hilt does not make any requirements, but the return value must be OkHttpClient
, because we just want to provide an instance for the OkHttpClient
type.
Note that the difference is that this time we are not writing an abstract function, but a regular function. In this function, create an instance of OkHttpClient
in the normal way and return it.
Finally, remember to add the @Provides
annotation above the provideOkHttpClient()
function so that Hilt can recognize it.
Well, now if you want to inject OkHttpClient
in MainActivity
, just write:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
...
}
Code language: CSS (css)
Then you can use the okHttpClient
object anywhere in MainActivity
, and the code will run normally.
In this way, we solve the problem of dependency injection for classes of third-party libraries, but this problem can actually be further expanded.
Now fewer and fewer people use OkHttp directly, and more developers choose to use Retrofit as their network request solution, and Retrofit is actually based on OkHttp.
In order to facilitate the use of developers, we hope to provide an instance of the Retrofit type in the NetworkModule, and when creating a Retrofit instance, we can choose to make it depend on OkHttpClient
. How to write it? Very simple:
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
...
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}
}
Code language: PHP (php)
A provideRetrofit()
function is defined here, and then an instance of Retrofit
is created in the function in the usual way and returned.
But we noticed that the provideRetrofit()
function also receives an OkHttpClient
parameter, and we also rely on this parameter when creating the Retrofit
instance. So you may ask, how do we pass the OkHttpClient
parameter to the provideRetrofit()
function?
The answer is that there is no need to pass it at all, because the process is done automatically by Hilt. All we need to do is to ensure that Hilt knows how to get an instance of OkHttpClient
, which we already did in the previous step.
So, if you now write code like this in MainActivity
:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
...
}
Code language: CSS (css)
Absolutely no problem.
Hilt built-in components and component scope
We just skipped the @InstallIn
annotation when we were learning about dependency injection for interfaces and third-party classes, now it’s time to look back.
In fact, the name of this annotation is quite accurate. InstallIn
means installed. Then @InstallIn(ActivityComponent::class)
is to install this module into the Activity component.
Since it is installed into the Activity component, it is natural to use all the dependency injection instances provided by this module in the Activity. In addition, Fragment and View contained in Activity can also be used, but other places except Activity, Fragment and View cannot be used.
For example, if we use @Inject
in Service to perform dependency injection on fields of Retrofit
type, an error will be reported.
But don’t panic, there are solutions to this.
Hilt has a total of 7 built-in component types, which are used to inject into different scenarios, as shown in the following table

In this table, each component has a different scope. Among them, the dependency injection instance provided by ApplicationComponent
can be used in the whole project. Therefore, if we want the Retrofit
instance just provided in the NetworkModule to be able to perform dependency injection in the Service, we just need to modify it like this:
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
...
}
Code language: CSS (css)
In addition, related to Hilt’s built-in components, there is a concept called component scope, and we have to learn about its role.
Perhaps this behavior of Hilt is not what you expected, but it is true: Hilt will create a different instance for each dependency injection behavior.
This default behavior is indeed very unreasonable in many cases. For example, the instances of Retrofit
and OkHttpClient
that we provide, in theory, they only need one copy globally. It is obviously unnecessary to create different instances each time.
And changing this default behavior is actually very simple, with the help of the @Singleton
annotation, as follows:
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com")
.client(okHttpClient)
.build()
}
}
Code language: PHP (php)
This ensures that only one instance of OkHttpClient
and Retrofit
exists globally.
Hilt provides a total of 7 component scope annotations, which correspond to the 7 built-in components just now, as shown in the following table

That is, if you want to share an instance of an object program-wide, use @Singleton
. If you want to share an instance of an object in an Activity, as well as the Fragment and View contained within it, use @ActivityScoped
. And so on.
Also, instead of having to use the scope annotation in a Module, we can declare it directly above any injectable class. For example, we declare the Driver class as follows:
@Singleton
class Driver @Inject constructor() {
}
Code language: JavaScript (javascript)
This means that Driver will share the same instance in the global scope of the entire project, and dependency injection can be performed on the Driver class globally.
And if we change the annotation to @ActivityScoped
, it means that the Driver will share the same instance within the same Activity, and Activity, Fragment, and View can all perform dependency injection on the Driver class.
You may be curious, how is this inclusion relationship determined, and why classes declared as @ActivityScoped
can also be dependency injected in Fragment and View?

Regarding the definition of the containment relationship, we can see it at a glance by looking at the following picture:
To put it simply, after declaring a certain scope annotation for a class, the place where the arrow of the annotation can point can be dependency injected for the class, and at the same time share the same instance within the scope.
For example, an arrow annotated with @Singleton
can point anywhere. The arrow of the @ServiceScoped
annotation has nowhere to point, so it can only be used within the Service itself. The arrow of @ActivityScoped
annotation can point to Fragment and View.
In this way, you should have a solid grasp of Hilt’s built-in components and the knowledge of component scope.
Preset Qualifier
Compared with traditional Java development, Android development has its own particularity. For example, there is a concept of Context
in Android.
Indeed, there are too many places in Android development that depend on Context
, and whatever interface you call will require you to pass in the Context
parameter.
So, if there is a class we want to inject dependency, it depends on Context, how to solve this situation?
For example, now the constructor of the Driver class accepts a Context parameter as follows:
@Singleton
class Driver @Inject constructor(val context: Context) {
}
Code language: CSS (css)
Now you will get an error when you compile the project. The reason is very simple. The Driver class cannot be dependency injected because Hilt does not know how to provide the Context parameter.
Feels familiar, doesn’t it? It seems that we also encountered this problem when we let the Truck
class depend on the Driver
class. The solution at that time was to declare the @Inject
annotation on the Driver’s constructor, so that it can also be dependency injected.
But obviously, we can’t solve the problem in the same way here, because we don’t have the right to write the Context
class at all, so we certainly can’t declare the @Inject
annotation on its constructor.
Then you may think again, without the permission to write the Context
class, then we can use the @Module
method we just learned to provide dependency injection to the Context
in the form of a third-party class, right?
At first glance, this solution seems to be OK, but when you actually write it, you will find problems, for example:
@Module
@InstallIn(ApplicationComponent::class)
class ContextModule {
@Provides
fun provideContext(): Context {
???
}
}
Code language: CSS (css)
Here I have defined a ContextModule
and a provideContext()
function, and its return value is indeed Context
, but I don’t know how to write it next, because I can’t return an instance of Context.
That’s right, instances of system components like Context are created by the Android system. We can’t just go to new instances, so naturally we can’t use the solutions we learned earlier to solve them.
So how to solve it? Very simple, Android provides some preset Qualifiers, which are specially used to provide us with dependency injection instances of the Context type.
For example, in the Truck
class just now, you only need to add an @ApplicationContext
annotation before the Context
parameter, and the code can be compiled and passed, as shown below:
@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}
Code language: CSS (css)
This way of writing Hilt will automatically provide a Context
of Application type to the Truck
class, and then the Truck
class can use this Context to write specific business logic.
If you need is not the Context of the Activity type. There is no problem, Hilt also presets another Qualifier, we can use @ActivityContext
:
@Singleton
class Driver @Inject constructor(@ActivityContext val context: Context) {
}
Code language: CSS (css)
However, if you compile the project at this time, you will find an error. The reason is also easy to understand. Now our Driver is Singleton, that is, it can be used globally, but it relies on an Activity type Context, which is obviously impossible.
As for the solution, I believe you who have learned the previous topic must already know it. We will change the annotation above the Driver to @ActivityScoped, @FragmentScoped, @ViewScoped
, or just delete it, so that you will not get an error when compiling again. .
There is actually a hidden trick about the preset Qualifier, that is, for the two types of Application and Activity, Hilt also presets the injection function for them. In other words, if one of your classes depends on Application or Activity, you don’t need to find a way to provide instances of dependency injection for these two classes, Hilt will automatically recognize them. As follows:
class Driver @Inject constructor(val application: Application) {
}
class Driver @Inject constructor(val activity: Activity) {
}
Code language: CSS (css)
This way of writing compilation will be passed directly without adding any annotation declaration.
Note that the two types must be Application and Activity. Even if they declare their subtypes, the compilation will not pass.
Then you may say that my project will provide some global and general functions in the custom MyApplication, which leads to many places relying on MyApplication written by myself, and MyApplication cannot be recognized by Hilt. In this case, it is necessary to How to do it?
Here I teach you a little trick, because there is only one instance of Application globally, so the Application instance injected by Hilt is actually your custom MyApplication instance, so you can do a down-type conversion.
For example, here I define an ApplicationModule
, the code is as follows:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}
}
Code language: CSS (css)
It can be seen that the provideMyApplication()
function receives an Application parameter, which is automatically recognized by Hilt, and then we can downcast it to MyApplication
.
Next you can declare dependencies in the Truck class like this:
class Driver @Inject constructor(val application: MyApplication) {
}
Code language: CSS (css)
Dependency Injection for ViewModel
The first way is to write by hand purely using what we have learned before.
Let’s say we have a Repository class to represent the repository layer:
class Repository @Inject constructor() {
...
}
Code language: JavaScript (javascript)
Since Repository
needs to be injected into ViewModel
, we need to add @Inject
annotation to Repository’s constructor.
Then there is a MyViewModel
that inherits from ViewModel
to represent the ViewModel
layer:
@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
...
}
Code language: CSS (css)
Note the following three points here.
- First, the header of MyViewModel should declare the @ActivityRetainedScoped annotation for it. Referring to the component scope table, we know that this annotation is specially provided for ViewModel, and its life cycle is also consistent with ViewModel.
- Second, the @Inject annotation should be declared in the constructor of MyViewModel, because we also use dependency injection to obtain an instance of MyViewModel in the Activity.
- Third, the Repository parameter should be added to the constructor of MyViewModel, indicating that MyViewModel is dependent on the Repository.
The next step is very simple, we get the instance of MyViewModel
by dependency injection in MainActivity
, and then use it as usual:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
...
}
Code language: CSS (css)
This works fine, but the downside is that we’ve changed the normal way of getting ViewModel
instances. Originally, I just wanted to perform dependency injection on Repository
, and now even MyViewModel
has to follow dependency injection.
For this reason, Hilt provides an independent dependency injection method for a common Jetpack component such as ViewModel
, which is the second method we will introduce next.
This way we need to add two additional dependencies to the app/build.gradle
file:
dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}
Code language: JavaScript (javascript)
Then modify the code in MyViewModel
as follows:
class MyViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {
...
}
Code language: CSS (css)
Notice the changes here, first of all the @ActivityRetainedScoped
annotation is gone because we don’t need it anymore. Secondly, the @Inject
annotation has become the @ViewModelInject
annotation, which can be seen from the name, this annotation is specially used for ViewModel
.
Now back to MainActivity
, you no longer need to use dependency injection to get the instance of MyViewModel
, but to get it completely according to the conventional writing method:
@AndroidEntryPoint class MainActivity : AppCompatActivity() { val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) } ... }
It looks exactly the same as the way we usually use ViewModel
. This is all the magic that Hilt has done for us behind the scenes.
It should be noted that in this way of writing, although we do not use the dependency injection function in MainActivity
, the annotation @AndroidEntryPoint
is still indispensable. Otherwise, Hilt does not detect syntactic exceptions at compile time, and once at runtime, Hilt cannot find the entry point and cannot perform dependency injection.
What about unsupported entry points?
Hilt supports a total of 6 entry points, which are:
- Application
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
The reason for this setting is that our program basically starts from these entry points.
For example, an Android program cannot start executing code from the Truck
class out of thin air, but must start executing from one of the above entry points, and then execute the code in the Truck class.
The working principle of Hilt starts from the onCreate()
method of Application, which means that all functions of Hilt cannot work properly before this method is executed.
It is for this reason that Hilt did not include ContentProvider
into the supported entry points.
However, even if the ContentProvider
is not the entry point, we still have other ways to use dependency injection inside it, it’s just a little more cumbersome.
First, you can customize an entry point of your own in ContentProvider
, and define the type to be dependency injected in it, as follows:
class MyContentProvider : ContentProvider() { @EntryPoint @InstallIn(ApplicationComponent::class) interface MyEntryPoint { fun getRetrofit(): Retrofit } ... }
As you can see, here we define a MyEntryPoint
interface, and then use @EntryPoint
above it to declare that this is a custom entry point, and use @InstallIn
to declare its scope.
Then we define a getRetrofit()
function in MyEntryPoint
, and the return type of the function is Retrofit
.
Retrofit
is a type that we have supported dependency injection, and this function has been completed as early as NetworkModule
.
Now, if we want to get an instance of Retrofit
in a function of MyContentProvider
(in fact, the network function is unlikely to be used in ContentProvider
, here is just an example), just write it like this:
class MyContentProvider : ContentProvider() { ... override fun query(...): Cursor { context?.let { val appContext = it.applicationContext val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java) val retrofit = entryPoint.getRetrofit() } ... } }
With the EntryPointAccessors
class, we call its fromApplication()
function to get an instance of the custom entry point, and then call the getRetrofit()
function defined in the entry point to get an instance of Retrofit
.