Android jetpack dependency injection framework - hit Getting Started Guide

Posted by blyz on Sat, 25 Dec 2021 20:35:47 +0100

I Hilt introduction

Hilt is the product of secondary packaging of Android official on the basis of relying on Dagger. Students who have studied Dagger will find that as a dependency injection framework, Dagger has powerful functions, but the high learning threshold makes the landing cost of Dagger on Android very high. In order to solve the above problems, Android officially launched the hit framework with lower learning threshold.

Hilt provides a standard way to use dependency injection in applications by providing a container for each Android class in the project and automatically managing its life cycle.

For the source code shown in this article, please refer to: linux-link/HiltUseDemo (github.com)

II Dependency injection

Dependency Injection (DI) also has an easily dizzying concept - Inversion of Control (IoC).

Control inversion is essentially a new programming idea, not a technical implementation. It mainly describes the creation and management of Java development domain objects:

  • Control: refers to the power to create (instantiate and manage) objects;

  • Reversal: control is handed over to the external environment (Spring framework, IoC container);

In the traditional development method, many member variables need to be used in a class. These member variables need to be new in turn! In the development method based on IOC idea, the creation of objects helps us to instantiate objects and assign values through the corresponding IOC container (Hilt,Dagger2 framework).

Dependency injection means that objects are created through external injection. To facilitate our understanding of * * dependency injection, * * let's take a simple example:

class User {

    val simple = Simple()

    fun functionA() {
        val count = simple.functionB()
        if (count > 0) {
            // do something.
        } else {
            // do something.
        }
    }
}

Function a in the User class depends on the result of function B in the Simple class. In order to get the result of function B, we new a Simple class.

We transform the above example into the form of dependency injection: each instance of User receives a Simple object as a parameter in its constructor, rather than constructing its own Simple object at initialization.

class User constructor(val simple: Simple){

    fun functionA() {
        val count = simple.functionB()
        if (count > 0) {
            // do something.
        } else {
            // do something.
        }
    }
}

Dependency injection will provide the following advantages for our applications:

  • Reuse classes and separate dependencies: it's easier to replace the implementation of dependencies. Due to the inversion of control, code reuse is improved, and classes no longer control how their dependencies are created, but support any configuration.

  • Easy to refactor: dependencies become a verifiable part of the API Surface, so they can be checked when creating objects or compiling, rather than hidden as implementation details.

  • Easy to test: classes do not manage their dependencies, so when testing, you can pass in different implementations to test all different use cases

It can be seen from the above description that dependency injection and control inversion are different descriptions of the same concept. IOC is a software design idea, and DI is a specific implementation of this software design idea. Dependency injection is easier to understand than control inversion

III Hilt use

1. Introduce Hilt dependency

To use Hilt in Android, we first need to introduce the root directory build Introducing Hilt dependency into gradle

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.0"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
        // hilt plugin
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
    }
}

Then, in the build. Net of the app project Introducing Hilt dependency into gradle

plugins {
    ...
    id 'dagger.hilt.android.plugin'
}

android {
   ...
}

dependencies {
    ...
    implementation 'com.google.dagger:hilt-android:2.38.1'
    kapt "com.google.dagger:hilt-android-compiler:2.38.1"
}

Hilt uses Java 8 features. To enable Java 8 in your project, you also need to add the following code to app / build In the gradle file.

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = '1.8'
}

2. Hilt application

All applications using Hilt must contain an Application class with @ HiltAndroidApp annotation@ HiltAndroidApp will trigger Hilt's code generation operation. The generated code includes a base class of the Application, which acts as an Application level dependency container.

@HiltAndroidApp
class MyApp : Application() {

}

Then, don't forget to use Android manifest Introducing Application into XML

<application android:name=".MyApp" >

3. Inject the object into the Android class

After setting Hilt in the Application class and having Application level components. Other Android classes can use @ AndroidEntryPoint to indicate that this class will use Hilt for dependency injection.

@AndroidEntryPoint
class NormalActivity : AppCompatActivity() {}

Hilt currently supports the following Android classes:

  • Application (by using @ HiltAndroidApp)

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

If you use @ AndroidEntryPoint to annotate an Android class, you must also annotate Android classes that depend on that class. For example, if you annotate a Fragment, you must also annotate all activities that use the Fragment.

Hilt only supports activities inherited from < U > componentactivity < / u >, such as < U > appcompatactivity < / u >.
Hilt only supports inheritance from Android Fragment of fragment.
Hilt does not support inheriting from Android app. Fragment of fragment.

Add the @ Inject annotation to the constructor of the target object to be injected.

import javax.inject.Inject

class Target @Inject constructor() {

    fun print() {
       ...
    }
}

Then introduce the target object into the Android class, and also add the @ Inject annotation. It should be noted here that objects annotated with * * @ Inject * * cannot be modified by * * private * *, otherwise compilation errors will occur.

import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class NormalActivity : AppCompatActivity() {

    @Inject
    lateinit var target: Target

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        println(target.print())
    }
}

Through the above steps, we used Hilt to complete the simplest injection of objects into Android classes. You can see that Hilt is much simpler to use than Dagger.

4. Use @ Mdoue to set the Hilt Module

Sometimes, types cannot be injected through constructors. This can happen for a variety of reasons. For example, you cannot inject an interface through a constructor. In addition, we can't inject types that don't belong to us through constructors, such as classes from external libraries. In these cases, you can use the Hilt module to provide binding information to Hilt.

Hilt module is a class with @ Module annotation. Like the Dagger Module, it tells hilt how to provide certain types of instances. Unlike the Dagger Module, you must use @ InstallIn to annotate the Hilt module to tell hilt which Android class each Module will be used or installed in.

Use @ Module to indicate that this is a Hilt Module, and @ InstallIn(ActivityComponent::class) to indicate that the objects provided in the current Module can be used in all activities of the application.

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

    @Provides
    fun providerTarget3(): Target3 {
        val target = Target3.Builder()
            .setStr("str")
            .build()
        return target
    }
}

5. Use @ Provides to inject instances

When an object comes from an external library or must be created using the builder mode, we cannot Inject an instance of the object by adding @ ` ` Inject to the constructor. At this time, we will use @ Provides.

For example, there is a Target3 class whose constructor is modified by private (@ Inject cannot Inject the constructor modified by private). We can only create Target3 class through build() provided in the class

class Target3 private constructor() {

    private lateinit var string: String

    fun print() {
        println(this.javaClass.simpleName + ":" + string)
    }

    class Builder {
        private val target = Target3();

        fun setStr(string: String): Builder {
            target.string = string
            return this
        }

        fun build(): Target3 {
            return target
        }
    }
}

The method with @ Provides informs Hilt of the following information:

  • The method return type tells the Hilt function which type of instance to provide.

  • Method parameters will inform Hilt of the corresponding type of dependency.

  • The method body tells Hilt how to provide instances of the corresponding type. Whenever you need to provide an instance of this type, Hilt executes the method body

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

    @Provides
    fun providerTarget3(): Target3 {
        val target = Target3.Builder()
            .setStr("str")
            .build()
        return target
    }
}

Then we can use the Target3 object in the Activity

@AndroidEntryPoint
class ProviderActivity : AppCompatActivity() {

    @Inject
    lateinit var target3: Target3

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        target3.print()
    }
}

6. Inject Context

The Target2 class needs to use Context, Activity and Target3 in its logical implementation

class Target2 constructor(
    val context: Context,
    val activity: Activity,
    val target: Target3
) {

    fun print() {
        println(this.javaClass.simpleName + "\n" + context + "\n" + activity + "\n")
        target.print()
    }
}

For context, we can use @ ActivityContext and @ ApplicationContext to specify whether we need the context of Activity or Application.

There is no need to add any annotation to the Activity because @ InstallIn(ActivityComponent::class) exists. Hilt will inject the Activity corresponding to the target2 scope for us.

Target3 objects are also automatically injected for us by providerTarget3().

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

    @Provides
    fun providerTarget3(): Target3 {
        val target = Target3.Builder()
            .setStr("str")
            .build()
        return target
    }

    @Provides
    fun providerTarget2(
        @ActivityContext context: Context,
        activity: Activity,
        target3: Target3
    ): Target2 {
        return Target2(context, activity, target3)
    }
}

The use of Target2 is very simple, as shown below.

@AndroidEntryPoint
class ProviderActivity : AppCompatActivity() {

    @Inject
    lateinit var target2: Target2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        target2.print()
    }
}

7. Inject the interface instance using @ bindings

In actual development, we often use an interface instance. The interface has no construction method. At this time, we can inject an interface through @ bindings@ The bindings annotation tells Hilt which implementation to use when it needs to provide an instance of the interface.

interface ISimple {

    fun print(string: String)

}

Define an abstract Module and inject the implementation class of the interface into it, as shown below

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

    @Binds
    abstract fun providerISimple(impl: ISimpleImpl): ISimple

}

Methods annotated with @ bindings provide Hilt with the following information:

  • The method return type tells the Hilt function which interface instance to provide.

  • Method parameters tell Hilt which implementation to provide.

Add @ Inject annotation on the constructor of the implementation class of the interface:

class ISimpleImpl @Inject constructor() : ISimple {

    override fun print(string: String) {
        println(this::class.simpleName + ";" + string)
    }

}

It is also very simple to use.

@AndroidEntryPoint
class AbsActivity : AppCompatActivity() {

    @Inject
    lateinit var simple: ISimple

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        simple.print("ISimple!!")
    }
}

Abstract class injection is exactly the same as interface injection. Please refer to the source code provided at the end of the article.

8. Inject different instances of the same type

Sometimes, in projects, we often use different instances of the same class. If the Module returns methods of the same type, we need to do some additional operations.

First, we need to use @ Qualifier to define an annotation to distinguish different Target object instances.

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

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

Then use the defined annotation in the Module to distinguish different Target injection methods

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

    @TargetType1
    @Provides
    fun providerTarget4Type1(): Target4 {
        return Target4();
    }

    @TargetType2
    @Provides
    fun providerTarget4Type2(): Target4 {
        return Target4();
    }
}

Finally, annotations should also be added to distinguish different object instances.

@AndroidEntryPoint
class MultiActivity : AppCompatActivity() {

    @TargetType1
    @Inject
    lateinit var target4Type1: Target4

    @TargetType2
    @Inject
    lateinit var target4Type2: Target4

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_multi)
        println(target4Type1)
        println(target4Type2)
    }
}

9. Hilt is used in conjunction with ViewModel

Hilt supports the injection of multiple object types, such as the ViewModel we most often use. Before injecting the ViewModel, we need to introduce additional dependencies.

dependencies {
    ...
    implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
    kapt "androidx.hilt:hilt-compiler:1.0.0"
    // It is not necessary, but it will facilitate us to use the KTX method of ViewModel
    implementation 'androidx.activity:activity:1.1.0'
    implementation 'androidx.fragment:fragment-ktx:1.2.5'
}

Mark the ViewModel with @ HiltViewModel in the ViewModel and add @ Inject on the construction method.

@HiltViewModel
class SimpleViewModel @Inject constructor(
    val repository: SimpleRepository
) : ViewModel() {

    fun getData() {
        repository.getData()
    }

}

Since SimpleRepository is introduced into the construction method of ViewModel, we need to add the @ ` ` Inject annotation to the construction method of SimpleRepository.

class SimpleRepository @Inject constructor() {

    fun getData() {
        println("getData")
    }
}

Finally, the SimpleViewModel is introduced into the Activity.

@AndroidEntryPoint
class ViewModelActivity : AppCompatActivity() {

    private val viewModel: SimpleViewModel by viewModels()

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

IV Scope of component

By default, all bindings in hilt are not scoped. This means that whenever a request binding is applied, hilt creates a new instance of the required type. Hilt also allows binding to be scoped to specific components. Hilt only creates a scoped binding for each instance of the component to which the binding scope is limited, and all requests for the binding share the same instance.

Android classGenerated componentsScope
ApplicationApplicationComponent SingletonComponent@Singleton
View ModelActivityRetainedComponent@ActivityRetainedScope
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View with @ WithFragmentBindings annotationViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

For example: if we want the instance of the object to be a Singleton. First, you need to specify that the Module is installed on the SingletonComponent, and then specify its scope as @ Singleton in the provider method, so that there will only be one instance of Target5 in the life cycle of the App.

@InstallIn(SingletonComponent::class)
@Module
object AppModule3 {

    @Singleton
    @Provides
    fun providerTarget():Target5{
        return Target5()
    }
}

For example, if we want an instance of an object to have only one instance in the life cycle of an Activity, we can specify its scope as @ ` ` ActivityScoped.

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

    @ActivityScoped
    @Provides
    fun providerTarget():Target5{
        return Target5()
    }
}

V Component lifecycle

Hilt will automatically create and destroy instances of the generated component classes according to the life cycle of the corresponding Android classes.

Generated componentsCreation timingDestruction timing
ApplicationComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()On view destruction
ViewWithFragmentComponentView#super()On view destruction
ServiceComponentService#onCreate()Service#onDestroy()

be careful:
ActivityRetainedComponent still exists after configuration changes, so it is created the first time Activity#onCreate() is called and destroyed the last time Activity#onDestroy() is called.

Vi summary

Through the above description, we have finished the basic way of using Hilt in Android. Please refer to the source code of this article: linux-link/HiltUseDemo (github.com)

VII reference material

Dependency injection in Android | Android developers | Android Developers (google.cn)

Hilt dependency injection framework Getting Started Guide_ Peterp's blog - CSDN blog_ hilt

Topics: Android jetpack