oneWhy use Hilt

Hilt is a dependency injection library for Android. Answering the question why use Hilt is simply answering: What is a dependency injection library? Why use a dependency injection library? What problem does Hilt solve?

What is dependency injection?

Dependency Injection is also known as Dependency Injection (DI). Hilt, Dagger, Koin, etc are dependency injection libraries. Dependency injection is primarily about decoupling and testing code.

Classes often need to reference other classes; for example, a Car class might need to reference an Engine class; these required classes are called dependencies.

Classes can get dependencies in one of three ways:

  1. Class to construct the dependencies it needs
  2. Grab from other places, for exampleContext gettergetSystemService()
  3. Provided as parameters, also known as dependency injection, based on the inversion of control principle. Provide these dependencies when constructing a class, or pass them into a function that requires the individual dependencies.
class Car {

    private val engine = Engine()

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

The above example constructs an instance of the Engine object in the Car class, which can be problematic:

  1. CarwithEngineToo close.CarOf a typeEngineAnd can’t easily use subclasses or alternative implementations. ifCarBuild your ownEngine, you must create two types ofCarAnd not directly will be the sameCarReused forGasElectricType of engine.
  2. rightEngineMakes testing more difficult.

If you use dependency injection

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

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

Car depends on Engine, so the application creates an instance of Engine and then uses it to construct an instance of Car.

  1. reuseCar. Can beEngineDifferent implementations of the incomingCar.
  2. Easy testCar. You can pass in test surrogates to test different scenarios.

The role of dependency injection libraries

There are two main types of dependency injection in Android:

  1. Constructor injection

    Pass a class’s dependencies to its constructor, as shown in the previous example

  2. Field injection

    Some Android framework classes, such as activities and fragments, are instantiated by the system and therefore cannot be constructor injected. With field injection, the dependency is instantiated after the class is created.

    class Car {
        lateinit var engine: Engine
    
        fun start(a) {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.engine = Engine()
        car.start()
    }
    Copy the code

We can create, provide, and manage different classes of dependencies ourselves, namely manual dependency injection. As the number of dependencies and classes increases, this approach can be particularly cumbersome and cause problems

  1. For large applications, getting all the dependencies and wiring them properly can require a lot of boilerplate code
  2. If you cannot construct a dependency before it is passed in, you need to write and maintain a custom container that manages the dependency life cycle in memory

Dagger is a popular dependency injection library for Java, Kotlin, and Android that is maintained by Google. Dagger creates and manages dependency diagrams for you to use DI in your application. It provides fully static and compile-time dependencies that solve many of the development and performance problems of reflectance-based solutions such as Guice.

Why useHiltDependency injection library

Hilt is built on top of the popular DI library Dagger and therefore benefits from Dagger’s compile-time correctness, runtime performance, scalability, and Android Studio support. Hilt provides a standard way to use DI (dependency injection) in your application by providing a container for each Android class in your project and automatically managing its life cycle.

To put it simply, the threshold is low and easy to use, and official blessing.

Two, basic use

With the Hilt dependency injection library, component-specific dependencies are introduced first.

For details about Hilt dependency introduction and configuration, see the official documentation

2.1 Hilt components

Before introducing the use of common Hilt annotations, take a look at the components of Hilt.

HiltThe component hierarchy

Hilt provides a set of built-in components that are automatically integrated into the various life cycles of Android applications.

The annotations above the component are scoped annotations that are bound to the lifecycle of the component. The binding of a child component can depend on any binding in the parent component.

When you define a binding in the @Installin module, the scope of the binding must match the scope of the component. For example, binding within @installin (ActivityComponent.class) module can only use @activityScoped.

HiltComponent injection

When injecting your own Android class using Hilt’s @AndroidEntryPoint, the Hilt component acts as an injector to determine which bindings are visible to the Android class

component The object for which the injector is oriented
SingletonComponent Application
ViewModelComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View with @WithFragmentBindings
ServiceComponent Service

Note: Hilt does not provide a component for Broadcast Receivers, because Hilt injects broadcast Receivers directly from SignletonComponent.

HiltComponent life cycle

The life cycle of a component is usually limited to the creation and destruction of the corresponding instance of an Android class

component The scope of Founded in Destroyed in
SingletonComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScoped Activity#onCreate()1 Activity#onDestroy()1
ViewModelComponent @ViewModelScoped ViewModelcreate ViewModeldestroy
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() Viewdestroy
ViewWithFragmentComponent @ViewScoped View#super() Viewdestroy
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()

By default, all bindings in Hilt are unqualified, meaning that Hilt creates a new instance of the required type each time an application requests a binding. Hilt allows a binding to be scoped to a particular component, and Hilt creates a scoped binding only once for each instance of the component to which the binding is scoped, sharing the same instance for all requests to that binding.

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }
Copy the code

Using @activityScoped to limit the scope of the AnalyticsAdapter to ActivityComponent, Hilt will provide the same instance of the AnalyticsAdapter for the entire lifecycle of the corresponding Activity.

Note:

It can be costly to limit the scope of a binding to a component because the supplied object remains in memory until the component is destroyed. Use scoped bindings as little as possible in your application

If the internal state of the binding requires that the same instance be used within a scope, or if the binding is expensive to create, scoping the binding to a component is appropriate.

Each Hilt component comes with a set of default bindings that can be injected as dependencies into your own custom bindings.

component The default binding
SingletonComponent Application
ActivityRetainedComponent Application
ViewModelComponent SavedStateHandle
ActivityComponent Application.Activity
FragmentComponent Application.Activity.Fragment
ViewComponent Application.Activity.View
ViewWithFragmentComponent Application.Activity.Fragment.View
ServiceComponent Application.Service

ActivityRetainedComponent exists in configuration changes, so it is in the first when onCreate and onDestroy last created

2.2 Applications and entry points

Common Hilt annotations include @HiltAndroidApp, @AndroidEntryPoint, @Inject, @Module, @installin, @provides, and @EntryPoint.

application@HiltAndroidApp

Every Android Application has an Application, which can be customized or default by the system. In Hilt, you must customize an Application or Hilt will not work properly.

@HiltAndroidApp
class MyApplication : Application() {}Copy the code

The @HiltAndroidApp annotation triggers the generation of Hilt code as the base class of the Application dependency container, which generates Hilt components that are attached to the Application lifecycle

The entry point@AndroidEntryPoint

After setting up @HiltAndroidApp in your Application, you can use @AndroidEntryPoint to enable member injection in your Android class. AndroidEntryPoint generates a separate Hilt component for each Android class in the project. These components can receive dependencies from their respective parent classes.

The types of @AndroidEntryPoint available are:

  1. Activity
  2. Fragment
  3. View
  4. Service
  5. BroadcastReceiver

ViewModel is supported through a separate Api @hiltViewModel

When you use @AndroidEntryPoint to annotate an Android class, you must also annotate the Android classes that depend on that class. For example, if you add annotations to a Fragment, you must add annotations to all activities using the Fragment; otherwise, exceptions will be thrown

java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
Copy the code

Take a look at a usage example

class Truck @Inject constructor() {fun deliver(a) {
        println("Deliver Banner public account: Wenjing Zhai")}}Copy the code
@AndroidEntryPoint
class HiltFirstActivity:AppCompatActivity() {
    private lateinit var mBinding:ActivityHiltFirstBinding

    @Inject
    lateinit var mTruck: Truck

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        mBinding = ActivityHiltFirstBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mTruck.deliver()
    }
}
Copy the code

Activity only supports subclasses of ComponentActivity, such as FragmentActivity and AppCompatActivity.

Fragment Supports only fragments that inherit androidx.Fragment.

Instance injection@Inject

Inject is used to tell Hilt how to provide instances of the class. It is commonly used in constructors, non-private fields, and methods. Note in particular that fields to be injected cannot be declared private.

class Truck @Inject constructor() {fun deliver(a) {
        println("Deliver Banner public account: Wenjing Zhai")}}Copy the code

Use the @inject annotation in the constructor of the class to tell Hilt how to provide instances of the class.

If the constructor has parameters

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

    fun deliver(a) {
        println("Truck is delivering cargo. Driven by $driver")}}Copy the code
class Driver @Inject constructor(a)Copy the code

In a class, the parameters of the annotated constructor are the class’s dependencies.

The aforementioned Driver is a dependency of Truck, and Hilt needs guidance on how to provide instances of the Driver. Truck can be dependency injected only if all other objects that depend on it in its constructor support dependency injection

2.3 the Hilt module

Sometimes, types cannot be injected through constructors. For example, you can’t inject interfaces through constructors, and you can’t inject types that you don’t own, such as classes from external libraries. In these cases, the Hilt module can be used to provide binding information to the Hilt.

A Hilt Module is a class with an @Module annotation that tells Hilt how to provide instances of certain types. Hilt modules must be annotated using the @installin annotation to tell Hilt which Android class each module will be used in or installed in.

2.3.1@ Sharing An example of an interface

An interface that does not have a constructor and cannot be injected via a constructor should provide binding information to Hilt, which is to create an abstract function with an @Curzer-binding annotation within the Hilt module that tells Hilt which implementation to use when it needs to provide interface instances.

A function annotated @ Cursor-Sharing should provide Hilt with the following information:

  1. Function return type: let us knowHiltThe function provides an instance of the interface
  2. Function parameters: let us knowHiltWhich implementation to provide

Take a look at the code example

interface Engine {
    fun start(a)
    fun shutdown(a)
}
Copy the code

First, the implementation class of the interface is implemented through dependency injection

class GasEngine @Inject constructor() : Engine {
    ...
}

class ElectricEngine @Inject constructor() : Engine {
    ...
}
Copy the code

Then, create a new abstract class that provides instances of the Engine interface in this module

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
    @Binds
    abstract fun bindEngine(gasEngine: GasEngine): Engine
}
Copy the code
  • inEngineModuleDeclare one above the@ModuleAnnotation representing the module used to provide dependency injection instances
  • The @installin annotation is used to determine which Hilt component to install the module into.
  • An abstract function is defined because no concrete function body is required. It doesn’t matter what the function name is
  • The return value of the abstract function in the example must beEngineIs used to giveEngineType interfaces provide instances; The supplied instances are determined by the receiving parameters of the abstract function

Finally, inject an instance into the Truck class

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

    @Inject
    lateinit var engine: Engine

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

}
Copy the code

@Module

@Module is often used to create objects that depend on classes (such as third-party libraries OkHttp, Retrofit, and so on) and interface instance injection. Classes that use the @Module annotation need to specify the scope of the Module with the @installIn annotation.

@InstallIn

Classes injected with @Module need to specify the scope of Module with the @InstallIn annotation. For example, a Module annotated with @Installin (ActivityComponent:: Class) is tied to the activity lifecycle, which means that all dependencies in the Module can be used in all activities within the application.

@Binds

The @Sharing annotation tells Hilt which implementation to use when it needs to provide an interface instance. The interface’s implementation class needs to be explicitly specified in the method parameters.

2.3.2 @provides Injects third-party class instances

If you don’t own a class or have to use the builder pattern to create instances (for example, Retrofit, OkHttp, Room, etc.) that cannot be injected through constructors, you can proactively tell Hilt how to provide instances of this type by creating a function within the Hilt module, Annotate the function with the @provides annotation.

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

  1. Function return type: let us knowHiltWhich type of instance the function provides
  2. Function parameters: let us knowHiltCorresponding type of dependency
  3. The function body: let us knowHiltHow to provide instances of the corresponding type. When you need to provide instances of this type,HiltThe function body is executed

OKHttpClinet, for example

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

    @Provides
    fun provideOkHttpClient(a): OkHttpClient {
        return OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .build()
    }

}
Copy the code
  • The function name doesn’t matter; in the example, the return value must be OkHttpClient because an instance of the OkHttpClient type is provided

  • In the function body, create the OkHttpClient instance as usual

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient
    ...

}
Copy the code

If you want to provide an instance of a Retrofit type in NetworkModule, you can choose to have it rely on OkHttpClient when creating an instance of Retrofit

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {...@Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("https://zhewendev.github.io/")
        .client(okHttpClient)
        .build()
    }

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

    @Inject
    lateinit var retrofit: Retrofit
    ...

}
Copy the code

As explained in the Hilt Component section, by default, all bindings in Hilt are unqualified, meaning that Hilt creates a new instance of the required type each time an application requests a binding. Instances of Retrofit and OkHttpClient theoretically need only one copy globally, and this behavior can be changed with the @Singleton annotation

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

    @Singleton
    @Provides
    fun provideOkHttpClient(a): 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("https://zhewendev.github.io/")
            .client(okHttpClient)
            .build()
    }
    
}
Copy the code

2.3.3 Providing multiple bindings for the same type

If you want Hilt to provide different implementations of the same type in the form of dependencies, you must provide multiple bindings to Hilt.

The bindEngine() function in EngineModule provides instances of the Engine interface, either GasEngine or ElectricEngine. The bindEngine() function provides two different instances of the Engine interface. To solve this problem, qualifiers are needed.

The @qualifier is an annotation that you can use to identify a specific binding for a type when more than one binding is defined for that type.

The @qualifier annotation is used to inject different instances of the same type of class or interface

Take a look at the code example

Start by defining the qualifier that you want to use to annotate the @Binds or @provides methods

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

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
Copy the code
  • @Retention: Used to declare the scope of annotations.

    Choose AnnotationRetention. BINARY said the annotation in the compiled will be retained, but not through reflection to access the annotations.

Then, Hilt needs to know how to provide an instance of the type corresponding to each qualifier

@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

Finally, get the corresponding type instance

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

    @BindGasEngine
    @Inject
    lateinit var gasEngine: Engine

    @BindElectricEngine
    @Inject
    lateinit var electricEngine: Engine

    fun deliver(a) {
        gasEngine.start()
        electricEngine.start()
        println("Truck is delivering cargo. Driven by $driver")
        gasEngine.shutdown()
        electricEngine.shutdown()
    }
}
Copy the code

The example defines two fields, gasEngine and electricEngine, both of type Engine. But above gasEngine, the @bindGasEngine annotation is used, so Hilt will inject it with an instance of gasEngine; ElectricEngine similarly.

2.3.4 Predefined qualifiers

A lot of Android development depends on the Context, and if we want to rely on the injected class, it depends on the Context, how do we solve this situation?

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

Compiling the project here gives an error because you don’t know how to supply the context parameter.

Android provides a number of predefined qualifiers specifically designed to give us dependency injection instances of the Context type

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

In this way, Hilt will automatically provide an application-type Context to the Truck class

If you need an ActivityContext, Hilt also presets another Qualifier, which you can use with @activityContext

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

The Driver is Singleton, which means it can be used globally, but it relies on an Activity Context, which is obviously not possible. Change the annotation to @activityScoped, @fragmentScoped, @viewscoped, or just delete it so that the re-compilation will not fail.

Hilt also presets injection capabilities for the Application and Activity types. That is, if you have a class that depends on an Application or Activity, you don’t need to provide an instance of dependency injection for those two classes; Hilt automatically recognizes them

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

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

Note that the two types must be Application and Activity; even declaring their subtypes will fail compilation.

If you provide some global generic functions in your custom MyApplication, so that many places depend on the written MyApplication, and the MyApplication is not recognized by HIlt, then you can do a downcast

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

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

}
Copy the code

Next, you can declare dependencies like this in the Truck class

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

2.4 ViewModel dependency injection

The ordinary way

For viewModels, instance injection can be implemented in the normal way using Hilt

For example, a Repository class represents the Repository layer

class Repository @Inject constructor() {... }Copy the code

And then there’s a MyViewModel that inherits from the ViewModel to represent the ViewModel layer

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

We then get an instance of MyViewModel in MainActivity via dependency injection

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel
    ...
    
}
Copy the code

This usage of field cannot be private; Also add LateInit, which is initialized later

In this way, the ViewModel is injected as a normal object, which will be reclaimed when the Activity is destroyed. It cannot be saved when the resource configuration changes

Independent dependency injection

Hilt provides a separate dependency injection method for common Jetpack components such as the ViewModel

Injection of ViewModel instance objects requires the @hiltViewModel annotation

@HiltViewModel
class FooViewModel @Inject constructor(
  val handle: SavedStateHandle,
  val foo: Foo
) : ViewModel
Copy the code
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
  private val fooViewModel: FooViewModel by viewModels()
}
Copy the code

2.5 Does not support injecting dependencies into classes

Hilt supports the most common Android classes. However, there may be times when you need to perform field injection in classes that Hilt does not support.

Hilt supports a total of six entry points, but Hilt is missing a key Android component: ContentProvider, due to lifecycle issues.

A 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.

In these cases, you can create entry points using the @entryPoint annotation.

First, you can define your own entry point in the ContentProvider, and then define the type of dependency injection you want

class MyContentProvider : ContentProvider() {

    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface MyEntryPoint {
        fun getRetrofit(a): Retrofit
    }
    ...
  
}
Copy the code

A getRetrofit() function is defined in MyEntryPoint and its return type is Retrofit. Retrofit is a type of dependency injection that we already support.

If we want to get an instance of Retrofit in a function of MyContentProvider, we just need to

class MyContentProvider : ContentProvider() {...override fun query(...).: Cursor { context? .let {val appContext = it.applicationContext
            val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java)
            valretrofit = 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

3. Working principle of Hilt

About the working principle of the Hilt can see working principle of the Hilt | MAD Skills, here no longer expand and redundancy