Jetpack learning-1-Hilt

Jetpack learning-1-Hilt

surroundings

AndroidStudio(4.2.1) kotlin(kotlin_version = "1.3.71")

Note: Compilation exception occurs when kotlin_version = "1.5.0".

Jetpack new member Hilt

Dependency Injection: Referred to as DI (Dependency Injection). Generally used for decoupling. Square launched the Dagger framework in 2012, based on Java reflection (time-consuming and difficult to use). Google developed fork and modified the Dagger code to Dagger2, based on annotations, to solve the drawbacks of reflection. Over-engineer some simple projects. Hilt draws on Dagger2 to make it simple and provides Android-specific APIs.

Introducing Hilt

Configure in the project and directory build.gradle:

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

In the app/build.gradle file:

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-compiler:2.28-alpha" } Copy code

To use Hilt, you must customize an Application, otherwise Hilt will not work. Customize MyApplication:

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

Then register MyApplication in the manifest file AndroidManifest.xml. Hilt greatly simplifies the usage of Dagger2, no need to write the bridge layer logic through the @Component annotation; it is limited to only start from a few fixed Android entrances. Hilt supports a total of 6 entrances:

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

Application entry point is declared with @HiltAndroidApp annotation. Other entry points are declared using @AndroidEntryPoint annotations. Activity entry point:

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

Inject into Activity:

@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 code

lateinit is a delayed initialization Kotlin syntax. @Inject annotation to inject truck member variables. Note that the fields injected by Hilt cannot be declared as private. Truck category:

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

Through the @Inject annotation, tell Hilt to create it through construction.

Dependency injection with parameters

The MainActivity above remains unchanged. Truck category:

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

Truck adds a Driver parameter through construction; declares an @Inject annotation on the Driver class constructor. All other objects depended on in Truck's constructor support dependency injection, so Truck can depend on dependency injection.

Dependency injection of interface

Engine interface:

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

Specific implementation classes: dependency injection, GasEngine and ElectricEngine

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

New abstract class to achieve bridging: EngineModule

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

@Module annotation provides dependency injection instance modules. Provide the instance required by the Engine interface. The abstract function name is customized. The abstract function does not need to be implemented. The return value of the abstract function must be Engine, which provides an instance for the Engine type interface. The abstract function receives any parameter and provides it with any instance. Adding @Bind to the abstract function is Hilt to be recognized. When changing the vehicle engine, you only need to modify the parameters of the abstract method bindEngnie in EngineModule.

Inject different instances of the same type

Qualifier annotation, 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 duplicated code

@Retention is used to declare the scope of the annotation. The AnnotationRetention.BINARY annotation will be saved after compilation, but the annotation cannot be accessed through reflection. EngineModule class modification:

@Module @InstallIn(ActivityComponent::class) abstract class EngineModule { @BindGasEngine @Binds abstract fun bindGasEngine (gasEngine: ElectricEngine ) : Engine @BindElectricEngine @Binds abstract fun bindElectricEngine (electricEngine: ElectricEngine ) : Engine } Copy code

Dependency injection modification 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 () { gasEngine.start() electricEngine.start() println( "run deliver by: $driver " ) gasEngine.shutdown() electricEngine.shutdown() } } Copy code

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

Dependency injection of third-party classes

For example: okhttp adds dependency in the app/build.gradle of the project:

Implementation "com.squareup.okhttp3: okhttp: 3.11.0" Copy Code

Define the class NetworkModule:

@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 code

Note that it is not an abstract class, because it is necessary to implement third-party class initialization, not an abstract function. Concrete realization of OkHttpClient creation. The function name is customized, and the return value must be OkHttpClient. The provideOkHttpClient function is annotated with @Providers so that Hilt can recognize it. When using:

@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 code

Solve the problem of third-party dependence, but fewer and fewer people use Okhttp, and more use Retrofit as a network request library, and Retrofit actually encapsulates Okhttp. We hope that Retrofit types are provided in NetworkModule:

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

Define a provderRetrofit function to create a Retrofit instance and return. providerRetrofit received an OkHttpClient parameter, which does not need to be passed. Because Hilt will find the OkHttpClient implementation of provideOkHttpClient on its own. Where it is used:

@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 code

Hilt built-in components and component scope

@InstallIn(ActivityComponent::class), install this module into the Activity component. Then Activity can use all dependency injection instances provided by this module. The Fragment and View contained in the Activity can also be used, but they cannot be used in other places except Activity, Fragment, and View. For example: Service uses @Inject to perform dependency injection on Retrofit type fields, and an error will be reported.

Hilt has 7 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

The scope of each component is different. The dependency injection provided by ApplicationComponent can actually be used in all projects. If you want the Retrofit instance provided by NetworkModule to be dependent in the Service, you can:

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

Hilt scope, each dependency injection behavior creates a different instance. In some cases it is unreasonable. In some cases, only one instance is used globally, and it is unreasonable to create each time. It can be handled by @Singleton.

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

It can be guaranteed that only one instance of OkhttpClient and Retrofit will exist globally. Class-component-scope

If you want to share an object instance in the whole program, use @Singleton. If you want to share an object instance in an Activity, and its internal Fragment and View, use @ActivityScoped. You don't have to use scope annotations in a Module, you can also declare it directly above any class, such as:

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

Driver shares an instance in the global scope of the entire project, and the driver can be dependency injected globally. If it is changed to @ActivityScoped, then Driver will share an instance within the same Activity, and Activity, Fragment, and View can all perform dependency injection on the Driver class. Containment relationship:

Preset Qualifier

If the class Driver requires the Context parameter:

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

Context Dirver Truck @Inject Context NetworkModule @Module Context

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

new Context Context Android @Qualifier Context @ApplicationContext

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

Hilt Application Context Truck Truck Context Application Context Activity Context Hilt Qualifier @ActivityContext

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

@ActivityScoped @Singleton ( ) @FragmentScoped @ViewScoped Qualifier Application Activity Hilt Application Activity Hilt

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

Application Activity MyApplication:

@Module @InstallIn(ApplicationComponent::class) class ApplicationModule { @Provides fun provierMyApplication(application: Application): MyApplication { return application as MyApplication } } class Driver @Inject constructor(val application: MyApplication) {}

provierMyApplication Application Hilt MyApplication Truck

ViewModel

MVVM Hilt

Repository:

class Repository @Inject constructor(){}

Repository ViewModel @Inject MyViewModel ViewModel ViewModel

@ActivityRetainedScoped class MyViewModel @Inject constructor(val repository: Repository):ViewModel(){}
  1. MyViewModel @ActivityRetainedScoped ViewModel ViewModel
  2. The constructor of MyViewModel should be declared as @Inject annotation, because the activity needs to use dependency injection to obtain the MyViewModel instance.
  3. The Repository parameter should be added to the MyViewModel constructor, indicating that MyViewModel is dependent on the Repository.

Obtain an instance of MyViewModel in MainActivity through 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 code

But there are disadvantages: originally I only wanted to inject the Repository dependency, but MyViewModel also dependency injection.

The second type:

We don't want ViewModel to follow dependency injection. Hilt specifically provides an independent dependency method, add dependency in app/build.gradle:

Implementation 'androidx.hilt: HILT-Lifecycle-ViewModel: 1.0.0-alpha02' kapt 'androidx.hilt: 1.0.0-alpha02: HILT-Compiler' duplicated code

Modify the MyViewModel code:

class MyViewModel @ViewModelInject constructor ( Val Repository: the Repository): the ViewModel () {} copy the code

@ActivityRetainedScoped annotation is removed, it is not needed. The @Inject annotation is changed to the @ViewModelInject annotation, which is specially used for ViewModel. In 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 code

This way of writing, although we did not use the dependency injection function in MainActivity, the @AndroidEntryPoint annotation cannot be less. Otherwise, Hilt cannot detect syntax exceptions during compilation, and during runtime, Hilt cannot find the entry point and cannot perform dependency injection.

Unsupported entry point

ContentProvider (one of the four major components) among the components supported by Hilt. The life cycle of ContentProvider is quite special. It is executed before the onCreate method of Application. Some third-party libraries use this method to initialize (Jetpack member App Startup). The principle of Hilt starts from the onCreate method of Application. Before this method is executed, all functions of Hilt cannot work normally. Hilt did not include ContentProvider into the supported entry point. Dependency injection function can be through other methods.

class MyContentProvider : ContentProvider (){ @EntryPoint @InstallIn(ApplicationComponent::class) interface MyEntryPoint { fun getRetrofit () :Retrofit } override fun onCreate () : 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 code

Define the MyEntryPoint interface, use @EntryPoint to declare the custom entry point, and use @InstallIn to declare the scope. The getRetrofit function is defined in MyEntryPoint, and the return type is Retrofit. The injection has been written above in Retrofit, and the initialization is completed in the NetworkModule. With the help of the EntryPointAccessors class, call the fromApplication function to obtain a custom entry point instance.