The environment

AndroidStudio (2) kotlin (kotlin_version = “1.3.71”)

Note: compile exception when kotlin_version = “1.5.0”.

New member of Jetpack Hilt

Dependency Injection (DI). It’s usually used to decouple. Square launched the Dagger framework in 2012, which is based on Java reflection (time-consuming and difficult to use). Google forked the Dagger code and changed it to Dagger2 based on annotations to solve the reflection problem. Overdesign some simple projects. Hilt borrows from Dagger2 to make it simple and provides An Android-specific API.

The introduction of Hilt

In the project and directory build.gradle configuration:

buildscript {
    dependencies {
        // 
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com. Google. Dagger hilt - android - gradle - plugin: 2.28 alpha'}}Copy the code

In app/build.gradle:

plugins {
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.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

With Hilt, you must customize an Application or Hilt will not work. Customize MyApplication:

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

Then register MyApplication in the manifest file androidmanifest.xml. Hilt greatly simplifies the use of Dagger2 by not using the @Component annotation to write bridge layer logic; Limited to a few fixed Android portals to start with. Hilt supports a total of 6 entries:

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

The Application entry point is declared using the @hiltAndroidApp annotation. All other entry points are declared using the @AndroidEntryPoint annotation. Activity entry point:

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

Inject:

@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

Lateinit is a delayed kotlin syntax. Inject the @inject annotation into the Truck member variable. Note that fields injected by Hilt cannot be declared private. Truck categories:

class Truck @Inject constructor() {fun deliver(a){
        println("run deliver")}}Copy the code

The @inject annotation tells Hilt to create by construct.

Parameterized dependency injection

The MainActivity above remains unchanged. Truck categories:

class Truck @Inject constructor(val driver: Driver){
    fun deliver(a){
        println("run deliver by:$driver")}}class Driver @Inject constructor() {}Copy the code

Truck adds a Driver parameter by construction; Declare an @Inject annotation on the Driver class constructor. Truck can only be dependency injected if all other objects that depend on the constructor for Truck support dependency injection.

Dependency injection of interfaces

Engine interface:

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

Implementation classes: dependency injection, GasEngine and ElectricEngine

class GasEngine @Inject constructor() : Engine {
    override fun start(a) {
        println("Gas start")}override fun shutdown(a) {
        println("Gas shutdown")}}class ElectricEngine @Inject constructor() : Engine {
    override fun start(a) {
        println("Electric start")}override fun shutdown(a) {
        println("Electric shutdown")}}Copy the code

New abstract class implementation bridge: EngineModule

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
    @Binds
    abstract fun bindEngine(gasEngine: GasEngine): Engine
}
Copy the code

The @module annotation provides the dependency injection instance Module. Provide the instances required by the Engine interface. The return value of an abstract function must be Engine, which provides an instance for an interface of Engine type. An abstract function is provided with an instance of whatever argument it receives. If you add @bind to an abstract function, it’s Hilt. When changing a vehicle’s engine, you just need to change the parameters of the abstract method bindEngnie in the EngineModule.

Inject different instances of the same type

Qualifier annotations to inject different instances of the same type of class or interface. Add two custom annotations:

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

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

@ Retention used to declare annotation scope, choose AnnotationRetention. BINARY annotation will be saved after the compilation, but can’t through reflection to access the annotation. Class EngineModule modified:

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

    @BindGasEngine
    @Binds
    abstract fun bindGasEngine(gasEngine: ElectricEngine): Engine

    @BindElectricEngine
    @Binds
    abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
Copy the code

Dependency injection changes for all Engine types:

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("run deliver by:$driver")
        gasEngine.shutdown()
        electricEngine.shutdown()
    }
}
Copy the code

Solved the problem of injecting different instances of the same type.

Dependency injection of third-party classes

For example, okhttp adds dependencies to your project’s app/build.gradle:

implementation "Com. Squareup. Okhttp3: okhttp: 3.11.0"
Copy the code

Define the class NetworkModule:

@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

Note that this is not an abstract class, because you need to implement third-party class initialization, not an abstract function. Concrete implementation OkHttpClient creation. The function name is custom and the return value must be OkHttpClient. The @providers annotation is added to the provideOkHttpClient function so that Hilt can identify it. In use:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println("okhttp-user:$okHttpClient")}}Copy the code

Third party dependencies are addressed, but fewer and fewer people are using Okhttp, and more are using Retrofit as a network request library, which Retrofit actually encapsulates Okhttp. We want a Retrofit type in NetworkModule:

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

    @Provides
    fun providerRetrofit(okHttpClient: OkHttpClient):Retrofit{
        return Retrofit.Builder()
            .baseUrl("http://baidu.com") .client(okHttpClient) .build(); }}Copy the code

Define a provderRetrofit function to create a Retrofit instance return. ProviderRetrofit receives an OkHttpClient parameter, which does not need to be passed. Because Hilt will find the OkHttpClient implementation of provideOkHttpClient itself. In use:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var retrofit: Retrofit

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println("retrofit-user:$retrofit")}}Copy the code

Hilt built-in components and component scopes

@installin (ActivityComponent::class) installs this module into the Activity component. The Activity can then use all of the dependency injection instances provided by this module. Fragments and views contained in an Activity can also be used, but not anywhere else except for activities, fragments, and Views. For example, an error is reported when the Service uses @Inject to Inject a Retrofit field.

Hilt comes with seven built-in component types:

  1. ApplicationComponent: Application
  2. ActivityRetainedComponent: ViewModel
  3. ActivityComponent: Activity
  4. FragmentComponent: Fragment
  5. ViewComponent: View
  6. ViewWithFragmentComponent: View annotated with @WithFragmentBindings
  7. ServiceComponent: Service

Each component has a different scope. The dependency injection provided by ApplicationComponent can actually be used throughout the project. If you want the Retrofit instances provided by NetworkModule to be dependent on services, you can:

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

Hilt scope, where a different instance is created for each dependency injection behavior. Unreasonable in some cases. In some cases, only one instance is used globally, and it is not reasonable to create each instance. This can be handled by @singleton.

@Module
@InstallIn(ApplicationComponent::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 providerRetrofit(okHttpClient: OkHttpClient):Retrofit{
        return Retrofit.Builder()
            .baseUrl("http://baidu.com") .client(okHttpClient) .build(); }}Copy the code

It is guaranteed that only one instance of OkhttpClient and Retrofit will exist globally. Class — component — scope

If you want to share an object instance across the program, use @Singleton. To use an object instance of an Activity with fragments and views inside it, use @activityScoped. You don’t have to use a scope annotation in a Module. You can declare it directly above any class, for example:

@Singleton
class Driver @Inject constructor() {}
Copy the code

Drivers share an instance globally across the entire project, and they can be dependency injected globally. If changed to @activityScoped, the Driver will share an instance within the same Activity, and activities, fragments, and Views can all inject dependencies into the Driver class. Inclusion relation:

Preset the Qualifier

If the class Driver needs Context:

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

You get an error because you don’t know who supplied the Context. Echo Dirver’s method referenced by Truck, annotating the constructor with @inject, is inoperable because it does not have permissions to write the Context class. Use NetworkModule @Module to provide dependency injection to the Context as a third party class:

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

But how do you do that, because you can’t just new a Context instance. Context is a system component, and instances are created by the Android system, so some of the previous methods cannot be implemented. You need to provide a dependency injection instance of Context through @qualifier. Add @ ApplicationContext

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

Hilt will automatically provide an application-type Context to the Truck class, which can then use the Context to write business logic. If you need an ActivityContext instead of an Application Context, Hilt is another Qualifier, @activityContext

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

I used @activityScoped above because @Singleton compiler reported an error (because it was global). You can also use @fragmentScoped, @viewscoped, or just delete it so that no errors are reported. Qualifier Hilt presets injection capabilities for Application and Activity types. If you rely on an Application or Activity and do not need to provide an instance of dependency injection, Hilt automatically recognizes:

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

Compilation can be done without adding any annotation declarations. Must be Application and Activity types, whose subtypes will not pass compilation. If the argument is MyApplication:

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

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

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

The provierMyApplication function receives an Application parameter, which Hilt automatically recognizes, and then converts it down to MyApplication. You can declare dependencies in the Truck class.

ViewModel dependency injection

In the MVVM architecture, it is appropriate for Hilt to manage the repository layer instance creation.

The first mode:

Warehouse layer Repository:

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

Repository relies on injection into the ViewModel, so annotate the constructor with @inject. MyViewModel inherits from ViewModel for the ViewModel layer:

@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository):ViewModel(){}
Copy the code
  1. MyViewModel is declared as an @ActivityRetainedScoped annotation, which is provided specifically for the ViewModel and has the same lifecycle as the ViewModel.
  2. Declare @Inject annotation in the constructor of MyViewModel because the Activity uses dependency injection to get the MyViewModel instance.
  3. The MyViewModel constructor takes the Repository argument to indicate that MyViewModel is dependent on Repository.

Get an instance of MyViewModel in MainActivity via dependency injection:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println("viewModel-user:$viewModel")}}Copy the code

There are drawbacks: MyViewModel is also dependency injected into Repository, which is intended only for Repository dependency injection.

The second:

We don’t want the ViewModel to follow dependency injection. Hilt specifically provides a standalone dependency method, adding dependencies to app/build.gradle:

    implementation 'androidx. Hilt: hilt - lifecycle - viewmodel: 1.0.0 - alpha02'
    kapt 'androidx. Hilt: hilt - compiler: 1.0.0 - alpha02'
Copy the code

Modify MyViewModel code:

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

The @activityRetainedScoped annotation is removed, it is not needed. The @Inject annotation is changed to the @ViewModelInject annotation, which is specifically used by ViewModel. In the MainActivity:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val viewModel:MyViewModel by lazy {
            ViewModelProvider(this).get(MyViewModel::class.java)
        }
        println("viewModel-user:$viewModel")}}Copy the code

In this way, although we don’t use dependency injection in MainActivity, the @AndroidEntryPoint annotation is essential. Otherwise, at compile time, Hilt cannot detect syntax exceptions, and at run time, Hilt cannot find entry points and cannot perform dependency injection.

Unsupported entry point

One of the components supported by Hilt is ContentProvider(one of the four components). The ContentProvider lifecycle is a special one, executed before the onCreate method of the Application, and initialized by some third-party libraries (Jetpack member App Startup). The principle of Hilt starts with the onCreate method of the Application, and all Hilt functions are not working properly until this method is executed. Hilt does not include ContentProvider as a supported entry point. You can rely on injection functionality in other ways.

class MyContentProvider :ContentProvider() {@EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface MyEntryPoint{
        fun getRetrofit(a):Retrofit
    }

    override fun onCreate(a): Boolean {
        println("provider==onCreate") context? .let {val appContext = it.applicationContext
            val entryPoint = EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java)
            val retrofit = entryPoint.getRetrofit()
            println("ContentProvider==retrofit:$retrofit")}return false}}Copy the code

Define the MyEntryPoint interface, using @EntryPoint to declare custom entry points and @installin to declare scopes. The getRetrofit function is defined in MyEntryPoint and the return type is Retrofit. Injection is written for Retrofit, and initialization is done in NetworkModule. With the EntryPointAccessors class, call the fromApplication function to get the custom entry point instance.