[toc]

The resources

Jetpack’s new member Hilt practices (1) Starting a pit

All aspects of Hilt and Koin performance were analyzed

New to Jetpack, this article takes you through Hilt and dependency injection

What is dependency injection

Let’s start with the following example code:

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}
Copy the code

You can see that the Car class creates its own Engine object instance to perform the start operation. So there’s a problem, if I want a regular car and an electric car I have to create two classes.

Ordinary cars:

class GasCar {

    private val engine = GasEngine()

    fun start() {
        engine.start()
    }
}
Copy the code

Electric vehicles:

class ElectricCar {

    private val engine = ElectricEngine()

    fun start() {
        engine.start()
    }
}
Copy the code

As you can see, the only difference between the two classes is the constructed Engine implementation, but we created two different classes, and the coupling between the two classes is too heavy. Let’s optimize it:

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val eEngine = ElectricEngine()
    val eCar = Car(eEngine)
    eCar.start()
    
    val gEngine = GasEngine()
    val gCar = Car(gEngine)
    gCar.start()
}
Copy the code

After optimization, Engine provides objects to Car in the form of parameters, thus achieving decoupling. This way of providing the objects needed to call the class as parameters is called dependency injection.

Dependency injection is about giving the caller what it needs. A dependency is something that can be called by a method. In the dependency injection form, the caller no longer obtains the dependency directly, but passes it to the caller through injection. The dependency is invoked by the caller only after injection. Passing dependencies to the caller, rather than letting the caller get them directly, is at the heart of dependency injection.

Why dependency injection

In summary, dependency injection’s biggest role is decoupling. By understanding the concept of dependency injection, we know that through dependency injection, the caller does not have to produce the dependent directly, and both sides are decouple. And because the creation of the dependent object instance is done by the dependency injection framework, there is less boilerplate code and the code is easier to understand. Controlling injected data through dependency injection makes testing easier. The DEPENDENCY injection framework can also automatically release objects when the caller’s life cycle ends to prevent memory usage.

Dagger, Hilt, and Koin are dependency injection frameworks. Hilt is a simple wrapper around the Dagger to make the framework easier to use. Koin is a lightweight dependency injection framework for Kotlin developers. For a comparison between Hilt and Koin, check out the following article:

All aspects of Hilt and Koin performance were analyzed

Let’s start with how Hilt is used.

Adding dependencies

Add the plugin to the project root build.gradle file:

buildscript { ... dependencies { ... Classpath 'com. Google. Dagger hilt - android - gradle - plugin: 2.28 alpha'}}Copy the code

Add the following code to your app and other build.gradle files using the Hilt module:

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

If you’re using Java development projects, you can omit the kotlin-kapt plugin and replace the kapt keyword with annotationProcessor.

Finally, since Hilt uses Java8 functionality, you need to add the following code to your app/build.gradle file to enable Java8 functionality:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
Copy the code

Explanation of the usage of each annotation

@HiltAndroidApp

To use Hilt, you must create the Application class and add the @hiltAndroidApp annotation. Then register the Application in the androidmanifest.xml file.

@HiltAndroidApp
class SampleApplication : Application()
Copy the code
<application
    android:name=".SampleApplication" >

</application>
Copy the code

@AndroidEntryPoint

Hilt currently supports the following six Android classes for injection:

  • Application(By using@HiltAndroidApp)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

The @AndroidEntryPoint annotation is required for all classes that require dependency injection except the @HiltAndroidApp modifier for Application.

@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {
}
Copy the code

Note:

  • HiltSupport for extensions onlyComponentActivityActivity, such asAppCompatActivity.
  • HiltSupport for extensions onlyandroidx.FragmentFragment.
  • HiltReserved not supportedFragment.

@Inject

Next, Inject can be performed with @inject:

@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {

    @Inject
    lateinit var capBean: CapBean
}
Copy the code

Note: Fields injected by Hilt cannot be private.

At this point, the injection of the capBean field is not complete, and Hilt does not know how to create instance objects. The @Inject annotation needs to be added to the constructor. At this point Hilt knows how to create instance objects that need to be injected.

class CapBean @Inject constructor()
Copy the code

Similarly, if the constructor of an injected object takes parameters, add @inject annotation to the constructor of the required parameters.

class CapBean @Inject constructor(val waterBean: WaterBean)

class WaterBean @Inject constructor()
Copy the code

@Module

There are cases where it is impossible to tell Hilt how to provide instances of the class or interface by adding @inject annotations to the constructor, such as:

  • Interface.
  • Classes from external libraries.

This can be done with the @Module annotation. The @Module annotation generates the Hilt Module, providing the concrete implementation in the Module.

@Binds

An interface instance is injected using the @Module + @Binds annotation.

First define an interface:

interface Water {
    fun drink()
}
Copy the code

Then define the implementation class of the interface:

class Milk @Inject constructor() : Water {
    override fun drink() {
        Log.e("water", "drink milk")
    }
}
Copy the code

Then define an abstract class with the @Module annotation:

@Module
@InstallIn(ApplicationComponent::class)
abstract class WaterModule {
}
Copy the code

Note: @Module must be shared with @installin to inform Hilt of the scope of each Module.

Creates an abstract function with the @Cursor-annotation in an abstract class:

@Module
@InstallIn(ApplicationComponent::class)
abstract class WaterModule {

    @Binds
    abstract fun bindsWater(milk: Milk): Water
}
Copy the code

A function with the @Cursor-binding annotation provides Hilt with the following information:

  • The return type of the function tellsHiltThe function provides an instance of the interface.
  • Function arguments will tell youHiltWhich implementation to provide.

At this point Hilt knows how to inject the concrete implementation of the interface.

@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {

    @Inject
    lateinit var water: Water
}
Copy the code
@Provides

If you don’t own a class, you can’t inject it through a constructor. The @Module + @provides annotation tells Hilt how to provide instances of this type.

Start by defining a common class that the @Module annotation decorates. This is a common class, not an abstract class, because we want to provide a concrete implementation of the class instance. Take OkHttpClient as an example:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
}
Copy the code

Add the @provides annotation. The body of the function Provides the specific creation of the OkHttpClient object instance:

@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

Functions annotated with @provides provide Hilt with the following information:

  • The return type of the function tellsHiltWhich type of instance the function provides.
  • Function arguments will tell youHiltCorresponding type of dependency.
  • The function body tells youHiltHow to provide instances of the corresponding type. Whenever an instance of that type is needed,HiltAll execute the function body.

Now you can inject through Hilt:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient
    ...
}
Copy the code

@Qualifier

Above, Milk, which provides Water interface through @ Cursorial, is successfully injected into the class. So if you need not only Milk implementation but also another Water interface implementation Juice in the call class. How do you deal with that?

First add Juice implementation:

class Juice @Inject constructor() : Water {
    override fun drink() {
        Log.e("water", "drink juice")
    }
}
Copy the code

Still add to @Module Module:

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

    @Binds
    abstract fun bindsMilk(milk: Milk): Water

    @Binds
    abstract fun bindsJuice(juice: Juice): Water
}
Copy the code

It is not possible to inject the calling class directly here:

@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {
    @Inject
    lateinit var juice: Water

    @Inject
    lateinit var milk: Water
}
Copy the code

Hilt has no way of distinguishing between two Water instances, so @qualifier is needed to define annotations to distinguish between instances of the same type.

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

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MilkWater
Copy the code

Then add the two defined annotations to the WaterModule function to tell Hilt how to provide different instances of the same instance:

@Module @InstallIn(ActivityComponent::class) abstract class WaterModule { @MilkWater @Binds abstract fun bindsMilk(milk:  Milk): Water @JuiceWater @Binds abstract fun bindsJuice(juice: Juice): Water }Copy the code

We also need to add the annotations we just defined in the calling class to distinguish between the two instances:

@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {

    @JuiceWater
    @Inject
    lateinit var juice: Water

    @MilkWater
    @Inject
    lateinit var milk: Water
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main_hilt)
        juice.drink()
        milk.drink()
    }
}
Copy the code

The output is as follows:

E/water: drink juice
E/water: drink milk
Copy the code

@ ApplicationContext, @ ActivityContext

This is where Hilt provides some predefined qualifiers.

  • @ApplicationContext: provideApplicationContext.
  • @ActivityContext: provideActivityContext.

Hilt components

The Hilt component is used to inject instances provided by Hilt into the corresponding Android class. Hilt components are used as follows:

@Module
@InstallIn(ActivityComponent::class)
object MainModule {

    @Provides
    fun provideCoffeeBean(): CoffeeBean {
        return CoffeeBean()
    }
}
Copy the code

Because the component set in the @Installin annotation is an ActivityComponent, this means that Hilt will provide an instance of the Activity through the MainModule.

@Installin

@installIn comments:

@Retention(CLASS) @Target({ElementType.TYPE}) @GeneratesRootInput public @interface InstallIn { Class<? >[] value(); }Copy the code

@installin contains a field with available values for the components supplied by Hilt that represent the target Android class to inject. The following table:

Hilt components The object for which the injector is oriented
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent Views with @WithFragmentBindings annotations
ServiceComponent Service
Component life cycle

Hilt automatically creates and destroys instances of generated component classes based on the life cycle of the corresponding Android class.

Note: ActivityRetainedComponent still exist after configuration changes, so it’s on the first call Activity# onCreate () is created, in the last call Activity# onDestroy () is destroyed.

Component scope

Note: modify the scope field need to delete the build/generated file under/source/kapt rerun.

By default, all instances in Hilt are not scoped. That is, Hilt provides a new instance each time a dependency is injected. As follows:

class UserBean @Inject constructor()
Copy the code
@AndroidEntryPoint
class MainHiltActivity: AppCompatActivity() {

    @Inject
    lateinit var firstUserBean: UserBean
    @Inject
    lateinit var secondUserBean: UserBean

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

        Log.e("hilt", firstUserBean.toString())
        Log.e("hilt", secondUserBean.toString())
    }
}
Copy the code

The output is as follows:

E/hilt: com.sample.hilt.UserBean@b01decb
E/hilt: com.sample.hilt.UserBean@dafe6a8
Copy the code

By setting scoped annotations, you can share the same instance in the corresponding Android class. As follows:

// Set the scope of the UserBean to Activity @activityScoped class UserBean @inject constructor()Copy the code

The output is as follows:

E/hilt: com.sample.hilt.UserBean@b01decb
E/hilt: com.sample.hilt.UserBean@b01decb
Copy the code

Hilt provides the following relationship between scoped annotations and corresponding Android classes:

Android class Generated components scope
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
Views with @WithFragmentBindings annotations ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped
Component hierarchy

An instance provided in a component hierarchy of the @Module Module can be used as a dependency in any of its children. As follows:

class WaterBean

class CapBean (val waterBean: WaterBean)
Copy the code
@Module
@InstallIn(ActivityComponent::class)
object HiltActivityModule {

    @Provides
    fun provideWaterBean(): WaterBean {
        return WaterBean()
    }
}
Copy the code
@Module
@InstallIn(FragmentComponent::class)
object HiltFragmentModule {

    @Provides
    fun provideCapBean(waterBean: WaterBean): CapBean {
        return CapBean(waterBean)
    }
}
Copy the code

HiltComponent hierarchy is as follows:

Component Default binding

Each Hilt component is bound by default to a set of Android classes. For example, the Activity component will inject all Activity classes, and each Activity class will have a different instance of the Activity component.

Inject dependencies into classes not supported by Hilt

There is no ContentProvider in the Android classes supported by Hilt. This is mainly because the ContentProvider has a unique lifecycle. It is executed before the onCreate() method of the Application, and Hilt works from the onCreate() method of the Application. This means that none of Hilt’s functions will work properly until this method is executed.

If you want to use Hilt in your ContentProvider to get some dependencies, you need to define an @entryPoint annotation interface for the types that require dependency injection. And declare its scope with the @installin annotation as follows:

class MyContentProvider : ContentProvider() { @EntryPoint @InstallIn(ApplicationComponent::class) interface ExampleContentProviderEntryPoint { fun  getWaterBean(): WaterBean } ... }Copy the code

The interface defines functions to get instances.

If you want to get the supplied instance, you need to get it via EntryPointAccessors, as follows:

class MyContentProvider : ContentProvider() { ... override fun query(...) : Cursor { context? .let { val entryPoint = EntryPointAccessors.fromApplication(it.applicationContext, MyEntryPoint::class.java) val waterBean = entryPoint.getWaterBean() } ... }}Copy the code