This article is published on my wechat official account at the same time. Scan the QR code at the bottom of the article or search for Guo Lin in wechat to follow it. The article will be updated every working day.

Good morning, everyone.

Finally to write such an article I am more afraid of the article.

Although this year’s Google I/O conference was not held due to the pandemic, Google is still releasing a lot of new technologies every year.

With the release of Android 11, the Jetpack family is welcoming new members, including Hilt, App Startup, Paging3, and more.

I’ve already written an article about App Startup before, and if you’re interested, you can refer to the new Jetpack members for an App Startup article.

The subject of this post is Hilt.

Hilt is a powerful and easy to use dependency injection framework, and arguably the most important addition to the Jetpack family this year.

So why say this is an article I am more afraid of the article? It’s hard to write about dependency injection. I think it’s easy to just show you how to use Hilt, but if you want to make sure you understand why you use Hilt? Or further, why use dependency injection? It’s not a very good topic to write about.

In this article, I will try to explain all of the above issues, and hopefully I can do it.

Also, note that the topic of dependency injection itself is non-linguistic, but since I’m going to cover Hilt in this article, all of the code in this article will be demonstrated using Kotlin. For those unfamiliar with Kotlin, check out my new book, First Line of Code, Android Edition 3.

Why use dependency injection?

The English name of Dependency Injection is DI. In fact, this is not a new term, but rather an old concept in software engineering.

If you’re talking about the most well-known application for dependency injection, it’s probably the Spring framework in Java. Spring started out as a framework for dealing with dependency injection, and has since evolved into a more comprehensive framework.

When I learned about Spring as a student, I had the same question that most developers have: why do we use DEPENDENCY injection?

Now I might have a better answer, in a word: decoupling.

Too much coupling can be a serious problem in your project, making it harder and harder to maintain later on.

To make it easier for you to understand, I’m going to give you a concrete example.

Let’s say we have a truck delivery company, and we currently have one truck that we use to make deliveries every day and make money to keep the company going.

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

To do this, we can write the following code:

class Truck {

    val computer1 = Computer()
    val computer2 = Computer()

    fun deliver() {
        loadToTruck(computer1)
        loadToTruck(computer2)
        beginToDeliver()
    }

}

Copy the code

Here’s a Truck, and there’s a deliver() function in the Truck that performs the delivery tasks. In the deliver() function, we load the two computers onto the truck and start the delivery.

Does this get the job done? Of course. Our task was to deliver two computers, and now that we have delivered both computers, our task is certainly completed.

But is there anything wrong with that? Yes, and it’s serious.

What’s the problem? For those of you who are observant, we created two instances of computers in the Truck class before delivering them. In other words, our trucks now have to be able to produce computers as well as deliver goods.

This is the problem of too much coupling, where the truck and the computer, which were not supposed to be connected, are coupled together.

If that doesn’t sound too bad, the next day the company received a new order for mobile phones, so the truck had to be able to make mobile phones. On the third day, another order for fruit and vegetables came in, so the truck could farm…

At the end of the day, it’s not a truck anymore, it’s a global center for manufacturing goods.

Now that we are aware of the magnitude of the problem, we can look back and ask ourselves where did our project go wrong?

This is a structural design problem. When you think about it, a truck doesn’t really care about what it’s delivering, it’s just delivering it. So you can think of the truck as being dependent on the goods, so if you give the goods to the truck, it delivers, and if you don’t give the goods to the truck, it waits.

With that said, we can change the code as follows:

class Truck {

    lateinit var cargos: List<Cargo>

    fun deliver() {
        for (cargo in cargos) {
            loadToTruck(cargo)
        }
        beginToDeliver()
    }

}

Copy the code

Now the Cargos field is added to the Truck class, which means that the Truck is dependent on the cargo. After this modification, our trucks no longer care about making any goods, but rely on whatever goods they deliver, and do what they are supposed to do.

In this way, we can call it dependency injection.

What does a DEPENDENCY injection framework do?

Now that the Truck class is reasonably designed, a new problem arises. If we were now the owners of a computer company, how would I get a truck to deliver my computers?

Is that hard to do? Many people can naturally write code like this:

class ComputerCompany { val computer1 = Computer() val computer2 = Computer() fun deliverByTruck() { val truck = Truck()  truck.cargos = listOf(computer1, computer2) truck.deliver() } }Copy the code

This code also works, but there are also serious problems with this code.

What’s the problem? It’s in the deliverByTruck() function, so in order for the truck to do the delivery for us, we built our own truck. This is obviously irrational. A computer company should only be making computers. It should not be making trucks.

So it makes more sense for us to call the truck delivery company and ask them to send a spare truck so we don’t have to build it ourselves. When the truck arrives, we load the computer onto the truck and carry out the delivery task.

This process can be shown in the following diagram:

Projects designed with this structure will have excellent extensibility. If another fruit and vegetable company needed to find a truck to deliver their food, we could use the same structure for the task:

Now, here’s the big deal. The part of calling a trucking company and asking them to arrange free vehicles can be done by hand or by using some dependency injection framework to simplify the process.

So, if you’re asking what a DEPENDENCY injection framework does, it’s really just to replace what’s shown below.

Hopefully, this gives you a sense of why we use DEPENDENCY injection and what a dependency injection framework does.

Does Android development need a dependency injection framework?

There is an argument that DEPENDENCY injection frameworks are primarily applied to server applications with high complexity, and that Android development often does not use dependency injection frameworks at all.

This may not seem wrong to me, but I would rather see the DEPENDENCY injection framework as a tool to help simplify code and optimize projects, rather than as an additional burden.

So, no matter how complex your program is, a DEPENDENCY injection framework can help you simplify your code and optimize your project.

Speaking of optimization projects, you might think that my example of trucks producing computers was hilarious. But believe it or not, in our actual development process, examples like this happen every day.

Do you write code in your Activity that creates instances that shouldn’t actually be created by the Activity?

For example, we all use OkHttp to make network requests. Have you ever created an instance of OkHttpClient in your Activity? If so, congratulations, you’re essentially asking the truck to produce the computer (Activity is the truck and OkHttpClient is the computer).

Of course, if it’s a simple project, we can create an instance of OkHttpClient in the Activity. Regardless of code coupling, even if the truck were to produce the computer, it wouldn’t be too much of a problem because it would work. At least for the time being.

The first time IT became clear to me that I urgently needed a dependency injection framework was when I was building projects using the MVVM architecture.

A schematic diagram of the MVVM architecture is available on the Android developer website, as shown below.

This is the Android application architecture that Google is recommending for us to use right now.

For those of you who haven’t been exposed to MVVM, let me give you a quick explanation of this picture.

This architecture diagram tells us that a well-architects project should have several layers.

The green part represents the UI control layer, and this part is the Activity and Fragment we usually write.

The blue part represents the ViewModel layer, which holds the data associated with UI elements and communicates with the repository.

The orange section represents the warehouse layer, which determines whether the data requested by the interface should be read from the database or fetched from the network, and returns the data to the caller. In short, the warehouse’s job is to do an assignment and scheduling job between local and network data.

In addition, all the arrows in the diagram are unidirectional. For example, the Activity points to the ViewModel, indicating that the Activity depends on the ViewModel, but the ViewModel cannot depend on the Activity. The same is true for the other layers, where an arrow represents a dependency.

Also, dependencies cannot cross layers. For example, the UI control layer cannot have dependencies with the repository layer. Components in each layer can only interact with its neighbors.

A project designed using this architecture, with a clear structure and clear hierarchy, is bound to be a very high code quality project.

However, in the process of implementing this architecture diagram, I found a problem.

In the UI control layer, the Activity is one of the four components, its instance creation is not to worry about.

In the ViewModel layer, Google provides a dedicated API in Jetpack to get ViewModel instances, so we don’t have to worry about instance creation.

But at the warehouse level, an awkward question arises: who should be responsible for creating instances of the warehouse? The ViewModel? No, the ViewModel only depends on the repository, it should not be responsible for creating instances of the repository, and other different ViewModels may depend on the same repository instance. The Activity? This is even worse, because Activity and ViewModel are usually one-to-one.

So in the end, I realized that no one should be responsible for creating instances of the repository, and that the easiest way to do this is to set up the repository as a singleton class so you don’t have to worry about instance creation.

A new problem with singletons is that the rule that dependencies cannot cross layers is broken. Since the repository has been set up as a singleton class, it is natural that everyone owns its dependencies, and the UI control layer can communicate directly with the repository layer, bypassing the ViewModel layer.

From a code design point of view, this is a very difficult problem to solve. But if we use a dependency injection framework, we can solve this problem very flexibly.

As you can see from the diagram above, the DEPENDENCY injection framework is there to help us call and arrange the free truck. I don’t care where the truck came from, as long as you can deliver it for me.

Therefore, the ViewModel layer should not care about where the repository instances come from, I just need to declare that the ViewModel is dependent on the repository and let the DEPENDENCY injection framework do the rest for me.

Do you understand the DEPENDENCY injection framework a little better by using this analogy?

Android’s usual dependency injection framework

Let’s talk about some of the most common dependency injection frameworks in Android.

In the early days, most Android developers had no awareness of using a DEPENDENCY injection framework.

In 2012, Square launched its still-well-known open source DEPENDENCY injection framework, Dagger.

Square has a number of very successful open source projects — OkHttp, Retrofit, LeakCanary, to name a few — and is used in almost every Android project. Dagger has no name, no project should be using it now, why?

So that’s an interesting story.

Dagger’s dependency injection concept, while very advanced, has a problem. It is implemented based on Java reflection, which leads to two potential pitfalls.

First, we all know that reflection is time-consuming, so using it in this way will reduce the efficiency of the program. This isn’t a big problem, of course, because today’s programs use reflection everywhere.

Second, the use of dependency injection frameworks in general is very difficult, and unless you are quite adept at using them, it is difficult to write them correctly in one go. However, with the implementation of dependency injection based on reflection, it is impossible to know at compile time whether the use of dependency injection is correct or not. It can only be determined at run time by whether the program crashes or not. This is very inefficient, and it is easy to hide some bugs deep.

Now that brings us to the interesting part, we all know that there are problems with the Dagger implementation, so Dagger2 is naturally designed to solve these problems. But Dagger2 wasn’t developed by Square. It was developed by Google.

This is a bit odd. Normally, versions 1 and 2 of a library are maintained by the same company or group of developers. How could Dagger1 and Dagger2 change so much? I don’t know why, but I noticed that the Dagger project Google is currently maintaining was forked from Square’s Dagger project.

So MY guess is that Google forks the Dagger source code, makes changes to it, and releases The Dagger2 version. When Square saw it, it decided that Google was doing so well that it didn’t need to do it again or maintain Dagger1, so it issued this statement:

So what makes the Dagger2 different from the Dagger1? The most important difference is that the implementation has changed completely. As we have already seen, Dagger1 is implemented based on Java reflection and has listed some of its drawbacks. Google’s Dagger2 implementation is based on Java annotations, which eliminates all of the disadvantages of reflection.

With annotations, Dagger2 automatically generates code for dependency injection at compile time, so it doesn’t add any runtime time. In addition, Dagger2 checks the developer’s dependency injection usage at compile time and fails directly if not, so that problems can be thrown as early as possible. In other words, as long as your project compiles properly, your dependency injection usage is basically fine.

So has Google’s Dagger2 been a success? It was a huge success.

According to Google, Dagger2 is used in 74% of the top 1,000 Google Play apps.

Here I should mention that overseas and domestic Android developers like to study the technology stack is not quite the same. Overseas, no one is working on homegrown Android technologies like hotfixes or plugins. So you may be wondering, what advanced courses do overseas developers learn?

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 you are a good developer.

Interestingly, however, not many people are willing to use Dagger2 in China. I have posted several articles about Dagger2 on my official account, but from the feedback, I feel that this technology is still relatively small in China.

While the Dagger2 is popular overseas, it’s notoriously complex and can be a drag on your project if you don’t use it well. So there has been talk of using Dagger2 overdesigning simple projects.

According to a survey released by the Android team, 49% of Android developers wish Jetpack offered a simpler dependency injection solution.

Google released Hilt this year.

Do you think after all these long speeches I’m finally getting to the point? Don’t think so. I think it’s more important to understand all of this than just to know how to use Hilt.

So, as we all know, the Dagger means a Dagger, and a dependency injection is like putting a Dagger straight into the spot where it needs to be inserted, hit the Dagger.

And Hilt is the meaning of the knife, it hides the sharpest place of the dagger, because if you don’t use the dagger well, you may injure yourself. Hilt provides you with a stable handle to ensure that you can use it safely and easily.

In fact, Hilt and Dagger2 are inextricably linked. Hilt is the Android team that contacted the Dagger2 team to develop a dependency injection framework for Android. Compared with Dagger2, the most obvious features of Hilt are: 1. Simplicity. 2. Provides an Android exclusive API.

Now, let’s learn how to use Hilt.

The introduction of Hilt

Before you can start using Hilt, you need to introduce Hilt into your current project. This process is a bit tedious, so follow the steps in the article step by step.

First, we need to configure the Hilt 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'}}Copy the code

As you can see, the latest version of Hilt is still in the alpha stage, but it doesn’t matter, I feel quite stable with it. I will update it after the official version is released, and there won’t be any big changes in usage.

Next, in the app/build.gradle file, introduce Hilt’s plugin and add Hilt’s dependency libraries:

. apply plugin: 'kotlin-kapt' apply plugin: 'dagger. Hilt. Android. The plugin dependencies {implementation "com. Google. Dagger: hilt - android: 2.28 - alpha" kapt Com. Google. "dagger hilt - android - the compiler: 2.28 - alpha"}Copy the code

The kotlin-kapt plug-in is also introduced here because Hilt is based on compile-time annotations, and to enable compile-time annotations, kotlin-kapt must be added first. If you are still developing projects in Java, you can omit this plugin and change the kapt keyword to annotationProcessor when adding annotation dependency libraries.

Finally, since Hilt will use Java 8 features, we need to enable Java 8 functionality 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
  }
}

Copy the code

Okay, so that’s all there is to configure. Now that you’ve successfully introduced Hilt into your projects, let’s learn how to use it.

A simple use of Hilt

Let’s start with the simplest function.

As you know, every Android Application has an Application. This Application can be customized or not. 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 define a MyApplication class that looks like this:

@HiltAndroidApp
class MyApplication : Application() {
}

Copy the code

You can write no code in your custom Application, but you must add a @hiltAndroidApp annotation, which is a prerequisite for using Hilt.

Next, register MyApplication in your AndroidManifest.xml file:

<application android: ... > </application>Copy the code

With that done, the next step is to use Hilt for dependency injection according to your specific business logic.

Hilt greatly simplifies the use of Dagger2 by eliminating the need to write the bridge layer logic through the @Component annotation, but it also limits the injection functionality to a few Android fixed entry points.

Hilt supports a total of 6 entry points, namely:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

Only the Application entry point is declared using the @hiltAndroidApp annotation, which we’ve just seen. All other entry points are declared with the @AndroidEntryPoint annotation.

For the most common Activity, if I want to do dependency injection in an Activity, I can declare the Activity as follows:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    
}

Copy the code

Let’s try to inject something into the Activity. What do I inject? Remember that truck, let’s try to inject it into the Activity.

Define a Truck class that looks like this:

class Truck {

    fun deliver() {
        println("Truck is delivering cargo.")
    }

}


Copy the code

As you can see, the truck currently has a deliver() method, indicating that it has a delivery capability.

Then modify the code in the Activity to look like this:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var truck: Truck

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        truck.deliver()
    }

}

Copy the code

The code here might look a little strange at first, but let me explain.

First of all, lateinit is a key word in Kotlin. It has nothing to do with Hilt. This keyword is used for late initialization of variables, because Kotlin initializes a variable when it is declared by default, and here we don’t want to initialize it manually, so lateinit. If you are developing in Java, you can ignore this keyword.

Next, we declare an @inject annotation above the Truck field, indicating that I want to Inject the Truck field through Hilt. If I had the analogy, it would be the computer company calling the truck delivery company to arrange a truck. We can think of MainActivity as a computer company that depends on the truck, but the computer company doesn’t care how the truck came from. Hilt’s role here is similar to that of a truck delivery company, figuring out how to arrange a vehicle and even having the obligation to build one.

In addition, fields injected by Hilt cannot be declared private.

But the code doesn’t work at this point, because Hilt doesn’t know how to provide a truck. Therefore, we also need to make the following modifications to the Truck class:

class Truck @Inject constructor() {

    fun deliver() {
        println("Truck is delivering cargo.")
    }

}

Copy the code

Here we declare an @inject annotation on the Truck constructor, essentially telling Hilt that you can arrange a Truck using this constructor.

Well, it’s that simple. Now you can run the program and you will see something like this in Logcat:

Which means the truck is actually making a good delivery.

Do you think it’s amazing? Instead of creating an instance of Truck in MainActivity, we declared it with @inject, and the result was to actually call its deliver() method.

This is the dependency injection functionality that Hilt gives us.

Dependency injection with parameters

Admittedly, the example we just gave is too simple and should be of limited use in real world programming scenarios, where this is not always the ideal.

So let’s step by step learn how to use Hilt for dependency injection in a variety of more complex scenarios.

The first one that comes to mind is, if my constructor takes arguments, how does Hilt do dependency injection?

We modified the Truck class as follows:

class Truck @Inject constructor(val driver: Driver) {

    fun deliver() {
        println("Truck is delivering cargo. Driven by $driver")
    }

}

Copy the code

As you can see, the Driver argument is now added to the Truck constructor to indicate that the Truck is dependent on a Driver, since the Truck won’t drive itself without a Driver.

So, since the truck is dependent on the driver, how does Hilt do dependency injection on the truck now? After all, Hilt had no idea where the driver was from.

This problem is not as difficult as it might seem, because since trucks are driver-dependent, if we want to do DEPENDENCY injection on trucks, naturally we need to be able to do DEPENDENCY injection on drivers first.

So we can declare the Driver class like this:

class Driver @Inject constructor() {
}

Copy the code

Quite simply, we declare an @inject annotation on the Driver class constructor, so that the Driver class becomes a dependency injection mode with no constructor.

Then there was no need to change any code, because now that Hilt knew how to dependency inject Driver, she knew how to dependency inject Truck.

In conclusion, Truck can be inject-dependent only if all other objects in the Truck constructor support DEPENDENCY injection.

Now run the program again and print the log as follows:

As you can see, the truck is now being driven by a driver whose ID number is DE5EDF5.

Dependency injection for the interface

With the constructor with a parameter out of the way, let’s move on to a more complex scenario: how to do dependency injection on an interface.

There is no doubt that we cannot do dependency injection on interfaces with the techniques we currently have, for the simple reason that interfaces have no constructors.

But don’t worry, Hilt has pretty good support for dependency injection for interfaces, so you’ll get the hang of it in no time.

Let’s continue with concrete examples.

Every truck needs an Engine to run normally, so here I define an Engine interface, as follows:

interface Engine {
    fun start()
    fun shutdown()
}

Copy the code

Quite simply, there are two methods to implement in the interface, one for enabling the engine and one for shutting it down.

Since there is an interface, there must be an implementation class. Here I define another GasEngine class and implement the Engine interface as follows:

class GasEngine() : Engine { override fun start() { println("Gas engine start.") } override fun shutdown() { println("Gas engine shutdown.") }}Copy the code

As you can see, we’ve implemented the ability to start and shut down the engine in GasEngine.

In addition, now new energy vehicles are very hot, Tesla has almost everywhere. So in addition to the traditional fuel engine, you now have electric engines. Here we define an ElectricEngine class and implement the Engine interface, as follows:

class ElectricEngine() : Engine {
    override fun start() {
        println("Electric engine start.")
    }

    override fun shutdown() {
        println("Electric engine shutdown.")
    }
}

Copy the code

Similarly, starting and shutting the engine is implemented in ElectricEngine.

As I said before, every truck needs an engine to run properly, that is to say, the truck is dependent on the engine. Now I want to inject the engine into the truck using dependency injection, so what do I have to write?

Based on what we’ve just learned, the most intuitive way to write it is this:

class Truck @Inject constructor(val driver: Driver) {

    @Inject
    lateinit var engine: Engine
    ...

}

Copy the code

We declare an engine field in Truck, which means that Truck is dependent on engine. The engine field is then injected using the @inject annotation above the engine 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 were a normal class, this would be fine. The problem is that Engine is an interface, and Hilt has no way of knowing how to create an instance of this interface, so it’s bound to get an error.

Here’s how to solve this problem step by step.

First, the two implementation classes I just wrote, GasEngine and ElectricEngine, are dependency injectable 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 {
    ...
}

Copy the code

This is the same technique we just learned, declaring the @inject annotation on the constructors of both classes.

Next we need to create a new abstract class. The name of the class can be anything, but it should be relevant to the business logic, so I suggest naming it Enginemodule.kt, as follows:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

}

Copy the code

Note that we need to declare an @Module annotation at the top of the EngineModule to indicate the Module that provides the dependency injection instance.

If you’ve studied Dagger2 before, this part will be quite easy to understand. It’s exactly the same as Dagger2.

If you haven’t studied Dagger2 before, don’t worry. Follow these steps and you’ll see how it works.

You may also notice that in addition to the @Module annotation, there is also an @installin annotation declared, which is something that Dagger2 does not have. I’ll use a separate topic later on to explain what the @installIn annotation does, but for now you just need to know it’s necessary.

With the EngineModule defined, the next step is to provide instances of the Engine interface in this module. How do you provide it? Very simple, the code looks like this:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

    @Binds
    abstract fun bindEngine(gasEngine: GasEngine): Engine

}

Copy the code

There are a few key points here that I want to illustrate one by one.

So first we’re going to define an abstract function, why an abstract function? Because we don’t have to implement the body of the function.

Second, it doesn’t matter what the name of this abstract function is, you won’t call it, but a good name will help you read and understand.

Third, the return value of the abstract function must be Engine, indicating that an instance is provided for an interface of type Engine. So what instances are provided to it? The abstract function is provided with whatever arguments it receives. Since our truck is more traditional and still uses a fuel Engine, the bindEngine() function takes the GasEngine parameter, that is, it provides an instance of GasEngine to the Engine interface.

Finally, add the @bind annotation above the abstract function so Hilt can recognize it.

After a bit of coding, let’s go back to the Truck class. As you can see, it makes sense to inject an instance of GasEngine into the engine field at this point, because with the EngineModule you just defined, it is clear that an instance of GasEngine will be injected into the engine field.

Is that actually the case? To find out, modify the code in the Truck class to look like this:

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()
    }

}

Copy the code

We start the engine before we start the delivery, and then we finish the engine after the delivery, very logical.

Now run the program again, and the console prints something like the following:

As expected, the fuel engine start and fuel engine shut down logs were printed before and after delivery, indicating that Hilt did inject an instance of GasEngine into the engine field.

This solves the problem of dependency injection for the interface.

Inject different instances of the same type

A friendly reminder, don’t forget that we haven’t used ElectricEngine yet.

Now that trucking companies are making a lot of money by delivering goods and feeding themselves, it’s time to think about the environment. Using a fuel engine for deliveries just isn’t green enough, so we decided to upgrade our trucks to save the planet.

But the current electric vehicle is not mature enough, there are short range, long charging time and other problems. What to do? So we’re going to take a middle ground and use a hybrid engine for the time being.

That means a truck will contain both a fuel engine and an electric engine.

We provide an instance of the Engine interface via the bindEngine() function in the EngineModule. This instance is either GasEngine or ElectricEngine. How can we provide two different instances of the same interface?

As you might imagine, I would define two different functions that take GasEngine and ElectricEngine arguments, as follows:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

    @Binds
    abstract fun bindGasEngine(gasEngine: GasEngine): Engine
    
    @Binds
    abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

Copy the code

This seems to make sense, but if you compile it, you’ll find an error:

Note the text in the red box. This error reminds us that Engine has been bound many times.

It makes sense to consider that we provide two different functions in the EngineModule, both of which return Engine. When you do dependency injection for the engine field in a Truck, do you use the instance provided by the bindGasEngine() function? Or do you use instances provided by the bindElectricEngine() function? Hilt couldn’t figure it out.

So this problem requires an additional technical tool: the Qualifier annotation.

The role of the Qualifier annotation is specifically to solve the problem we are currently experiencing, injecting different instances of the same type of class or interface.

Here we define two separate annotations, as follows:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine

Copy the code

One annotation is called BindGasEngine and the other is called BindElectricEngine, so the roles of the two annotations are clearly separated.

Also, there is no question that the annotation must be declared with @qualifier above it. As for another @ Retention, is used to declare annotation scope, select AnnotationRetention. BINARY said the annotation in the compiled will be retained, but can’t through reflection to access the annotations. This should be the most reasonable scope of an annotation.

With these two annotations defined, we go back to the EngineModule. You can now add the two annotations you just defined above the bindGasEngine() and bindElectricEngine() functions, 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

}

Copy the code

In this way, we have classified two functions that provide instances of the Engine interface, one under the @BindgasEngine annotation and one under the @BindelectricEngine annotation.

It’s not over yet, though, because with the addition of the Qualifier annotation, all places that do dependency injection for Engine types also need to declare annotations specifying which instances of the type they 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()
    }

}

Copy the code

Does this code look easy to understand now?

We define two fields, gasEngine and electricEngine, both of type Engine. But above gasEngine, the @bindgasengine annotation is used so that Hilt can inject instances of gasEngine into it. Above 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, then the electric engine, and after the delivery, we turn off the fuel engine first, then the electric engine.

What will be the end result? Run it, as shown in the figure below.

It was great. Everything worked as we expected.

This solves the problem of injecting different instances of the same type.

Dependency injection for third-party classes

I’ll leave the truck example for now, and let’s look at some more practical examples.

As mentioned earlier, if we want to make a network request using OkHttp in MainActivity, we usually create an instance of OkHttpClient. In principle, however, an instance of OkHttpClient should not be created by an Activity, so dependency injection is obviously a good solution. That is, let MainActivity rely on OkHttpClient.

The OkHttpClient class is provided by the OkHttp library. We don’t have access to the class, so it’s impossible to add @inject to the constructor of OkHttpClient.

This is where the @Module annotation comes in. The solution is similar to providing dependency injection for interface types, but not quite the same.

First, define a class called NetworkModule that looks like this:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
    
}

Copy the code

Its initial declaration is very similar to that of the EngineModule, except that we don’t declare it as an abstract class because we don’t define abstract functions here.

Obviously, in NetworkModule, we want to provide instances of the OkHttpClient type, so we can write code like this:

@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()
    }

}

Copy the code

Again, the provideOkHttpClient() function name is arbitrary. Hilt doesn’t require anything, but the return value must be OkHttpClient, since we are providing instances of the OkHttpClient type.

Notice the difference, this time we’re not writing an abstract function, we’re writing a regular function. In this function, create an instance of OkHttpClient as normal and return it.

Finally, remember to add the @Provides annotation above the provideOkHttpClient() function so Hilt can recognize it.

Ok, now if you want to dependency inject OkHttpClient in your MainActivity, just write:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient
    ...

}

Copy the code

Then you can use the okHttpClient object anywhere in the MainActivity, and the code will work fine.

This solves the problem of dependency injection for third-party library classes, but the problem could be extended a little further.

Now that fewer people are using OkHttp directly, more developers are choosing to use Retrofit, which is actually based on OkHttp, as their web request solution.

We want to provide an instance of Retrofit in the NetworkModule for developers to use. When we create a Retrofit instance, we can choose to make it dependent on OkHttpClient. 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() } }Copy the code

You define a provideRetrofit() function, then create an instance of Retrofit in the normal way, and return it.

However, we notice that the provideRetrofit() function also receives an OkHttpClient parameter, and we rely on this parameter when creating the Retrofit instance. So you might be asking, how do we pass OkHttpClient to the provideRetrofit() function?

The answer is, not at all, because the process is done automatically by Hilt. All we need to do is make sure Hilt knows how to get an instance of OkHttpClient, which we did a step earlier.

So, suppose you now write code like this in MainActivity:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var retrofit: Retrofit
    ...

}

Copy the code

There is absolutely no problem.

Hilt has built-in components and component scopes

We skipped the @installIn annotation when we learned about dependency injection for interfaces and third-party classes, so it’s time to take a look back.

The name of the annotation is accurate. InstallIn means “installed”. So @installIn (ActivityComponent::class) installs this module into the Activity component.

Since it is installed in the Activity component, it is natural that all dependency injection instances provided by this module can be used in the Activity. In addition, the fragments and views contained in the Activity can also be used, but they cannot be used elsewhere except in the Activity, Fragment, and View.

For example, if we use @inject in a Service to Inject a field of type Retrofit, we will always get an error.

But don’t panic. There are solutions.

Hilt has seven built-in component types for injection into different scenarios, as shown in the table below.

In this table, the scope of each component is different. ApplicationComponent provides dependency injection instances that can be used throughout the project. Therefore, if we want the Retrofit instance we just provided in the NetworkModule to be able to do dependency injection in the Service, we just need to change it like this:

@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
    ...
}

Copy the code

In addition to Hilt’s built-in components, there is a concept called component scope, which we will also learn about.

Hilt may not behave as you would expect, but it is true: Hilt creates a different instance for each dependency injection.

This default behavior is very unreasonable in many cases, such as the instances we provide of Retrofit and OkHttpClient, which theoretically only need one copy globally, and creating a different instance each time is obviously an unnecessary waste.

Changing the default behavior is as simple as the @Singleton annotation, as shown below:

@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()
    }
    
}

Copy the code

This ensures that there is only one instance of OkHttpClient and Retrofit globally.

Hilt provides a total of seven component scope annotations, which correspond to each of the seven built-in components, as shown in the table below.

That is, if you want to share an instance of an object programmatically, use @Singleton. Use @activityscoped if you want to share an instance of an object with an Activity and the fragments and views contained within it. And so on.

In addition, we don’t have to use scope annotations in a Module, we can declare them directly on top of any injectable class. For example, we declare the Driver class as follows:

@Singleton
class Driver @Inject constructor() {
}

Copy the code

This means that drivers share the same instance globally across the project, and that Driver classes can be injected globally.

If we change the annotation to @activityscoped, it means that the Driver will share the same instance within the same Activity, and that the Activity, Fragment, and View can all have dependency injection on the Driver class.

You may be wondering how this inclusion relationship is determined and why classes declared @activityscoped can also have dependency injection in fragments and views.

For the definition of an inclusion relationship, let’s look at the following diagram to see it clearly:

In simple terms, after you declare a scope annotation on a class, wherever the arrow of that annotation points, the class can be dependency injected, while sharing the same instance within that scope.

For example, the arrow in the @Singleton annotation can point anywhere. The arrow in the @servicescoped annotation has nowhere to point to, so it is restricted to the Service itself. The @activityscoped annotation can point to the Fragment or View.

This should give you a good grasp of Hilt’s built-in components and their scope.

Preset the Qualifier

Android development has its own particularity compared to traditional Java development, such as the concept of Context in Android.

People who are new to Android development may always wonder what a Context is, but people who have been doing Android development for years probably don’t even care. I use it every day, or even everywhere, and I’m numb to what a Context is.

Indeed, there is so much reliance on Context in Android development that every interface you call will require you to pass in the Context argument.

So, if we have a class that we want to inject, and it’s dependent on Context, how do we solve this situation?

For example, the Driver class constructor now takes a Context argument, as follows:

@Singleton
class Driver @Inject constructor(val context: Context) {
}

Copy the code

The Driver class can’t be injected because Hilt doesn’t know how to provide the Context argument.

Feels familiar, doesn’t it? It seems that we had the same problem when we made the Truck class depend on the Driver class. The solution was to declare the @inject annotation on the Driver constructor so that it could be injected as well.

Obviously, we can’t do the same here, because we don’t have writing privileges on the Context class at all, so we certainly can’t declare the @inject annotation on its constructor.

So you might think that without writing permission for the Context class, we can provide dependency injection to the Context as a third party class using the @Module method we learned earlier.

At first glance, this might seem like a good solution, but when you actually write it, you’ll find problems. For example:

@Module @InstallIn(ApplicationComponent::class) class ContextModule { @Provides fun provideContext(): Context { ??? }}Copy the code

Here I define a ContextModule and a provideContext() function, which returns Context, but I don’t know what to do because I can’t return an instance of Context.

For example, a Context is created by the Android system. You can’t just create an instance of a Context, so you can’t use the solution you learned earlier.

So what’s the solution? Quite simply, Android provides a number of pre-qualifiers that are specifically designed to provide us with a context-type dependency injection instance.

For example, in the Truck class, you only need to add an @applicationContext annotation before the Context argument to compile the code, as shown below:

@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}

Copy the code

This way Hilt will automatically provide an application-type Context to the Truck class, which can then use this Context to write specific business logic.

But if you say, I don’t need an Application Context, I need an Activity Context. Hilt also presets another Qualifier. We’ll just use @ActivityContext:

@Singleton
class Driver @Inject constructor(@ActivityContext val context: Context) {
}

Copy the code

But at this point, if you compile the project, you will find an error. Now our Driver is Singleton, that is, globally available, but relies on an Activity Context, which is obviously not possible.

As for the solution, I believe you already know from the previous topic, we can change the annotations above the Driver to @activityscoped, @fragmentscoped, @viewscoped, or delete them directly, so that the recompile will not report error.

One of the hidden tricks for pre-qualifiers is that for both Application and Activity types, Hilt also has pre-configured injection capabilities for them. That is, if you have a class that depends on an Application or an Activity, you don’t need to find a way to provide dependency injection instances for both classes. Hilt automatically recognizes them. As follows:

class Driver @Inject constructor(val application: Application) {
}

class Driver @Inject constructor(val activity: Activity) {
}

Copy the code

Compiling this way will be straightforward without adding any annotation declarations.

Note that the two types must be Application and Activity, and even declaring their subtypes will not pass compilation.

So you might say, well, my project will provide some global functions in my custom MyApplication, so I have to rely on MyApplication that I wrote myself, and MyApplication is not recognized by Hilt. What if?

Here’s a tip: Since there is only one global instance of the Application, the Application instance that Hilt injects is actually your own MyApplication instance, so do a downcast.

For example, here I define an ApplicationModule that looks like this:

@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {

    @Provides
    fun provideMyApplication(application: Application): MyApplication {
        return application as MyApplication
    }

}

Copy the code

As you can see, the provideMyApplication() function takes an Application parameter, which Hilt automatically recognizes, and then we convert it down to MyApplication.

You can then declare dependencies in the Truck class like this:

class Driver @Inject constructor(val application: MyApplication) {
}

Copy the code

Perfect solution.

Dependency injection for ViewModel

By now, you’ve covered almost all the important points in Hilt.

Let’s go back to where we started: in an MVVM architecture, who should create instances at the warehouse level?

Now do you have a better answer to that question?

After LEARNING Hilt, I have put this problem behind me. Obviously, according to the MVVM architecture diagram, the ViewModel layer only depends on the warehouse layer, it does not care where the instances of the warehouse come from, so it is appropriate for Hilt to manage the instance creation of the warehouse layer.

As for the specific how to achieve, I summed up about two ways, here respectively with you to demonstrate.

Note that the following code demonstrates only the parts of the MVVM architecture related to dependency injection. If you are not familiar with the MVVM architecture, or the Jetpack components, you may not understand the following code. For this part of the game, I suggest you look at chapters 13 and 15 of First Line of Code, Android Version 3.

The first way is to write by hand, purely using what we’ve learned before.

For example, we have a Repository class that represents the Repository layer:

class Repository @Inject constructor() {
    ...
}

Copy the code

Since Repository depends on injection into the ViewModel, we need to annotate @inject into the constructor of Repository.

There is then a MyViewModel that inherits from ViewModel to represent the ViewModel layer:

@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
    ...
}

Copy the code

Here are three points to note.

First, the MyViewModel header should declare an @activityRetainedScoped annotation for it. Referring to the component scope table, we know that this annotation is specifically provided for the ViewModel, and its lifecycle is the same as that of the ViewModel.

Second, the @inject annotation should be declared in the MyViewModel constructor, because we also need to use dependency injection to obtain the MyViewModel instance in the Activity.

Third, the MyViewModel constructor should add the Repository argument to indicate that MyViewModel is dependent on Repository.

The next step is as simple as getting an instance of MyViewModel in MainActivity via dependency injection and using it as usual:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel
    ...
    
}

Copy the code

This works fine, but the downside is that we change the way we normally get ViewModel instances. I wanted to inject Repository, but now I want to inject MyViewModel as well.

To this end, Hilt provides a separate approach to dependency injection for a common Jetpack component, the ViewModel, which we will cover next.

In this way we need to add two additional dependencies to the app/build.gradle file:

dependencies { ... Androidx. hilt:hilt-compiler:1.0.0-alpha02' kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'} androidx.hilt:hilt-compiler:1.0.0-alpha02'}Copy the code

Then modify the code in MyViewModel to look like this:

class MyViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {
    ...
}

Copy the code

Notice the changes here. First, the @activityRetainedscoped annotation is gone because we no longer need it. Second, the @Inject annotation becomes the @ViewModelInject annotation, which, as you can see from the name, is specifically for viewModels.

Now go back to MainActivity, and you don’t need to use dependency injection to get an instance of MyViewModel, but just do it the way it should be:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
    ...

}

Copy the code

It looks exactly like the way we would normally write a ViewModel, with Hilt doing the magic behind it.

Note that although we are not using dependency injection in MainActivity, the @AndroidEntryPoint annotation is still needed. Otherwise, Hilt does not detect syntax exceptions at compile time, and once at run time, Hilt cannot find an entry point and cannot perform dependency injection.

What about unsupported entry points?

At the beginning of learning Hilt, I mentioned that Hilt supports 6 entry points, which are:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

The reason why we do this is because we basically start our program from these entry points.

For example, an Android program would never start executing code from the Truck class, but would have to start at one of the above entry points to get the code executed in the Truck class.

But if you’ve noticed, one key Android component is missing from the entry point for Hilt support: the ContentProvider.

As we all know, the ContentProvider is one of the four components, and it can also be called an entry point, because the code can run directly from here, without having to go through calls from other classes to reach it.

So why doesn’t Hilt support a ContentProvider among its entry points? This is a question I also had a lot of doubts about, so at the GDG roundtable in Shanghai last time, I took this question directly to Yigit Boyar, after all, he is responsible for the Jetpack project at Google.

Of course, I got a pretty good answer, mainly because of the life cycle of contentProviders. If you are familiar with contentProviders, you should know that their life cycle is special. They are executed before the Application’s onCreate() method, so many people use this feature for early initialization. See Jetpack New members. App Startup.

Hilt starts in the onCreate() method of Application, which means that none of Hilt’s functions will work until this method is executed.

It is for this reason that Hilt has not included the ContentProvider as an entry point for support.

However, even if the ContentProvider is not the entry point, there are other ways to use dependency injection within it that are a little more cumbersome.

First, you can customize your own entry point in the ContentProvider and define the type you want to inject as follows:

class MyContentProvider : ContentProvider() {

    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface MyEntryPoint {
        fun getRetrofit(): Retrofit
    }
    ...
  
}

Copy the code

As you can see, here we define a MyEntryPoint interface, and then use @entrypoint above it to declare that this is a custom EntryPoint, and @installin to declare its scope.

We then defined a getRetrofit() function in MyEntryPoint, and the return type of the function is Retrofit.

Retrofit, on the other hand, is a type that we already support for dependency injection, which has been done since the NetworkModule.

Now, if we want to get an instance of Retrofit in a function of MyContentProvider (in fact, it’s unlikely that a web function is used in a ContentProvider, so this is just an example), we just need to write:

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() } ... }}Copy the code

With the EntryPointAccessors class, we call its fromApplication() function to get an instance of our custom entry point, and then call the getRetrofit() function defined in the entry point to get an instance of Retrofit.

However, I don’t think the custom entry point feature is often used in actual development, but it is included for knowledge integrity reasons.

At the end

This is the end of this article.

It took me about half a month to write, and it’s probably the longest article I’ve ever written.

Even though Hilt is a huge simplification of Dagger2, if you don’t know anything about DEPENDENCY injection, you’ll have a lot of trouble getting into Hilt.

I’ve done my best to explain what dependency injection is, why it is used, and how it is used, but GIVEN the complexity of the topic, I don’t know how easy this article is. I hope all the readers can master Hilt and use Hilt well.

In addition, because Hilt and Dagger2 are so closely related, some of the information we learn in this article is provided by Hilt and some comes with Dagger2 itself. However, I did not make a strict distinction in this article, and the unification was all told from Hilt’s perspective. Therefore, if you are familiar with Dagger2, please do not feel that the statement in this article is not rigorous enough, because too rigorous words may increase the understanding cost of readers who have not learned Dagger2.

Finally, I will use some code examples in this article, written a Demo program uploaded to GitHub, there is a need to directly download the source code can be friends.

Github.com/guolindev/H…

Follow my technical public number, every day there are quality technical articles push.