The relevant knowledge

Dependency injection

  • Dependency Injection, DI;
  • Dependency injection decouples code, making it easy to reuse, refactor, and test

What is dependency injection

  • Classes usually need to reference other classes, and you can get the objects you want in one of three ways:
  1. Create instances of the required dependencies in the class
class CPU () { var name: String = "" fun run() { LjyLogUtil.d("$name run..." ) } } class Phone1 { val cpu = CPU() fun use() { cpu.run() } }Copy the code
  1. From a parent class or another class
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkCapabilities = Cm. GetNetworkCapabilities (cm) activeNetwork) LjyLogUtil. D (" if there is a network connection: ${networkCapabilities = = null} ")Copy the code
  1. In the form of parameters, you can supply these dependencies when constructing a class or pass them into a function that needs the individual dependencies;
  • The third approach is dependency injection

Dependency injection in Android

Manual dependency injection
Constructor injection
Class Phone (private val CPU: CPU) { fun use() { cpu.run() } } val cpu = CPU() val phone=Phone(cpu) phone.use()Copy the code
2. Field injection (or setter injection)
  • Dependencies are instantiated after the class is created
Class Phone {lateinit var CPU: CPU fun use() = cpu.run() } val phone = Phone() val cpu = CPU() phone.cpu = cpu phone.use()Copy the code
  • Both of the above are manual dependency injection, but manual dependency injection can cause problems if there are too many dependencies and classes
1. More and more complicated use; 2. Generate a lot of template code; 3. Dependencies must be declared in order; 4. It is difficult to reuse objects;Copy the code
Automatic dependency injection framework
  • Some libraries solve this problem by automating the process of creating and supplying dependencies in several ways:
1. Join dependencies at run time via reflection; 2. Code that generates connection dependencies at compile time through annotations; 3. Kotlin's powerful syntactic sugar and functional programming;Copy the code
1. The Dagger:
  • The most widely known dependency injection framework in the Android space is well known
Dagger 1.x version: Square is implemented based on reflection, has two drawbacks: one is reflection time, the other is reflection is run time, no errors are reported at compile time. It is difficult to use, and it is often easy to write wrong at first contact, resulting in low development efficiency; Dagger 2.x version: Google implements Java annotations to solve the above problem perfectly,Copy the code
2. Koin
  • A lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin, it uses functional parsing only, no proxy, no code generation, and no reflection (implemented through Kotlin’s powerful syntactic sugar (Inline, Reified, etc.) and functional programming).
3. The Hilt:
  • Due to the complexity and difficulty of Dagger, the Android team and Dagger2 team jointly developed Hilt, a dependency injection framework specifically for Android. The most obvious features are: 1. Simple; 2. Provide an Android specific API; 3. Official Google support for use with other Jetpack components;

Hilt

  • Hilt defines a standard way to perform DI in your application by providing a container for each Android class in your project and automatically managing its life cycle for you.
  • Hilt is built on top of the popular DI library Dagger and therefore benefits from the compile-time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.

Hilt usage flow

Adding dependencies

//1. Configure the Hilt plugin path buildscript {... dependencies { ... Classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'}} //2. Plugins for Hilt {... id 'dagger.hilt.android.plugin' id 'kotlin-kapt' } //3. Add Hilt dependency library and Java8 Android {... CompileOptions {sourceCompatibility JavUncomfortable.VERSION_1_8 targetCompatibility javUncomfortable.VERSION_1_8} // For Kotlin KotlinOptions kotlinOptions {jvmTarget = "1.8"}} dependencies {... Implementation "com.google.dagger:hilt-android:2.28-alpha" kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"}Copy the code

Hilt application class

  • Annotate Application with @hiltAndroidapp;
  • The @HILtAndroidApp annotation triggers code generation for Hilt, including a base class for the application that acts as an application-level dependency container.
@HiltAndroidApp
class MyApplication : MultiDexApplication() {
    ...
}
Copy the code

Inject the dependency into the Android class

1. Annotate classes with @androidentryPoint;
  • There are currently 6 types of entry points supported: Application (via @hiltAndroidApp), Activity, Fragment, View, Service, and BroadcastReceiver
  • Using @AndroidEntryPoint to annotate Android classes, you must also annotate Android classes that depend on that class, such as annotating Fragments, You must also add an @AndroidEntryPoint annotation to the Activity on which the Fragment depends.
2. Annotate the execution field with @inject
  • AndroidEntryPoint generates a separate Hilt component for each Android class in the project. These components can receive dependencies from their respective parent classes. To obtain dependencies from components, perform field injection using the @inject annotation. Note that Hilt injected fields cannot be declared private;
3. Use @inject annotation in constructors
  • To perform field injection, we need to use the @inject annotation in the constructor of the class to tell Hilt how to provide instances of the class:
@AndroidEntryPoint class HiltDemoActivity : AppCompatActivity() { @Inject lateinit var cpu: CPU ... } class CPU @Inject constructor() { var name: String = "" fun run() { LjyLogUtil.d("$name run..." )}}Copy the code
Dependency injection with parameters:
  • How does Hilt do dependency injection if the constructor takes parameters?
  • All other objects that the constructor depends on need to support dependency injection
class CPU @Inject constructor() { var name: String = "" fun run() { LjyLogUtil.d("$name run..." ) } } class Phone @Inject constructor(val cpu: CPU) { fun use() { cpu.run() } } @AndroidEntryPoint class HiltActivity : AppCompatActivity() {@inject lateinit var phone: phone fun test() {phone.cpu.name = "kirin 990" phone.use()}}Copy the code

Hilt Module

  • When some type parameters, such as interfaces or classes from external libraries, cannot be injected through constructors, the Hilt module can be used to provide binding information to the Hilt.
  • The Hilt Module is a class annotated with @Module and scoped with @Installin
An example of an adaptive interface is used using @Sharing
Interface ICPU {fun run()} //2. Constructor () : ICPU {Override fun run() {ljylogutil.d (" Kylin run..." Class Phone @inject constructor(val CPU: ICPU) {fun use() {cpu.run()}} 4. Abstract Class CPUModel {@Sharing Abstract Fun bindCPU(CPU: KylinCPU): ICPU } //5. Use the injected instance @androidentryPoint class HiltActivity: AppCompatActivity() {@inject lateinit var phone: Phone fun test() {Phone.cpu. Name = "Phone 990" Phone. Use ()}}Copy the code
Inject the instance using @provides
  • If a class is not owned by you (because it comes from an external library, such as Retrofit, OkHttpClient, or Room database classes), or if you have to create an instance using the builder pattern, you cannot inject it through the constructor.
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModel {
    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient().newBuilder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .writeTimeout(90, TimeUnit.SECONDS)
            .build()
    }
}
Copy the code
Provide multiple bindings for the same type
  • For example, network requests may require OkHttpClient with different configurations, or Retrofit with different BaseUrl
  • It is implemented using the @qualifier annotation
Interface ICPU {fun run()} class KylinCPU @inject constructor() : ICPU { override fun run() { LjyLogUtil.d("kylin run..." ) } } class SnapdragonCPU @Inject constructor() : ICPU { override fun run() { LjyLogUtil.d("snapdragon run..." }} / / 2. To create multiple types of annotations @ the Qualifier @ Retention (AnnotationRetention. BINARY) the annotation class BindKylinCPU @ the Qualifier @ Retention (AnnotationRetention. BINARY) the annotation class BindSnapdragonCPU / / @ Retention: annotation life cycle / / AnnotationRetention. SOURCE: compile time only, not stored in a BINARY output / / AnnotationRetention BINARY: Stored in a binary output, but the reflection is not visible. / / AnnotationRetention RUNTIME: stored in a binary output, to reflect visible / / 3. The annotation @Module @Installin (ActivityComponent::class) Abstract Class CPUModel {@bindkylinCPU @Binds Abstract Fun is used in the Hilt Module bindKylinCPU(cpu: KylinCPU): ICPU @BindSnapdragonCPU @Binds abstract fun bindSnapdragonCPU(cpu: SnapdragonCPU): ICPU } //4. Class phone5@inject Constructor (@bindSnapDragonCPU private Val CPU: ICPU) { @BindKylinCPU @Inject lateinit var cpu1: ICPU @BindSnapdragonCPU @Inject lateinit var cpu2: ICPU fun use() { cpu.run() cpu1.run() cpu2.run() } fun use(@BindKylinCPU cpu: ICPU) { cpu.run() } }Copy the code

Component Default binding

  • Since a Context class from Application or Activity may be required, Hilt provides the @ApplicationContext and @ActivityContext qualifiers.
  • Each Hilt component comes with a set of default bindings that Hilt can inject as dependencies into your own custom bindings
class Test1 @Inject constructor(@ApplicationContext private val context: Context)

class Test2 @Inject constructor(@ActivityContext private val context: Context)
Copy the code
  • Hilt also presets injection capabilities for Application and Activity types (these are required, even if subclasses are not).
class Test3 @Inject constructor(val application: Application)

class Test4 @Inject constructor(val activity: Activity)
Copy the code

Hilt Built-in component type

  • When we used Hilt Module above, we used @installin (), which means which component to install the Module into
Hilt comes with 7 built-in components to choose from:
  1. ApplicationComponent: Corresponding to Application, dependency injection instances can be used throughout the project
  2. Corresponding ViewModel 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)
  3. ActivityComponent: corresponds to activities. Fragments and views contained in activities can also be used.
  4. FragmentComponent: indicates the Fragment
  5. ViewComponent: corresponding to View
  6. ViewWithFragmentComponent: corresponding with @ WithFragmentBindings annotation View
  7. ServiceComponent: corresponds to Service
  • The Hilt does not provide a component for broadcast Receivers because the Hilt injects broadcast Receivers directly from ApplicationComponent.

Component scope

  • Hilt creates a different instance for each dependency injection behavior by default.
Hilt has seven built-in component scope annotations
  1. Singleton: Corresponding to the ApplicationComponent, the entire project shares the same instance
  2. @ ActivityRetainedScope: ActivityRetainedComponent corresponding components
  3. ActivityScoped: the corresponding ActivityComponent will share the same instance within the same Activity (including its fragments and views)
  4. @fragmentScoped: Corresponding component FragmentComponent
  5. @ ViewScoped: corresponding ViewComponent and ViewWithFragmentComponent module;
  6. @ServicesCopedService: corresponds to ServiceComponent
  • For example, we often need a global OkhttpClient or Retrofit, which can be implemented as follows
interface ApiService { @GET("search/repositories? sort=stars&q=Android") suspend fun searRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse } @Module @InstallIn(ApplicationComponent::class) class NetworkModel { companion object { private const val  BASE_URL = "https://api.github.com/" } @Singleton @Provides fun provideApiService(retrofit: Retrofit): ApiService {return retrofit.create(ApiService::class.java)} // Component scope :Hilt creates a different instance for each dependency injection behavior by default. @Singleton @Provides fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .baseUrl(BASE_URL) .client(okHttpClient) .build() } @Singleton @Provides fun provideOkHttpClient(): OkHttpClient { return OkHttpClient().newBuilder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(90, TimeUnit.SECONDS) .build() } }Copy the code
  • Or use Room to manipulate a local database
@Database(entities = [RepoEntity::class], version = Constants.DB_VERSION) abstract class AppDatabase : RoomDatabase() { abstract fun repoDao(): RepoDao } @Module @InstallIn(ApplicationComponent::class) object RoomModule { @Provides @Singleton fun provideAppDatabase(application: Application): AppDatabase { return Room .databaseBuilder( application.applicationContext, AppDatabase::class.java, Constants.db_name).allowMainThreadQueries() // Allows queries in the main.build ()} @provides @singleton fun provideRepoDao(appDatabase: AppDatabase):RepoDao{ return appDatabase.repoDao() } }Copy the code

Dependency injection for ViewModel

  • Creating a Repository in a ViewModel is as follows:
{@inject constructor(){Inject constructor(){Inject constructor(){Inject constructor(){Inject constructor(); RepoResponse { return apiService.searRepos(1, }} //2. Constructor (private val repository) {//2. Repository): ViewModel() { var result: MutableLiveData<String> = MutableLiveData() fun doWork() { viewModelScope.launch { runCatching { withContext(Dispatchers.IO){ repository.getData() } }.onSuccess { result.value="RepoResponse=${gson().toJson(it)}" }.onFailure {result.value=it. Message}}}} //3. Activity layer @androidEntryPoint class HiltMvvmActivity: AppCompatActivity() { @Inject lateinit var viewModel: MyViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hilt_mvvm) viewModel.result.observe(this, Observer { LjyLogUtil.d("result:$it") }) lifecycleScope viewModel.doWork() } }Copy the code
ViewModel and @ViewModelInject annotations
  • This changes the normal way of getting ViewModel instances, and Hilt provides a separate dependency injection method for it: @ViewModelInject
Implementation 'Androidx.hilt :hilt-lifecycle- viewModel :1.0.0-alpha02' kapt 'androidx. Hilt: hilt - compiler: 1.0.0 - alpha02' / / 2. Modify MyViewModel: Remove the @activityRetainedScoped annotation, ViewModelInject class MyViewModel @viewModelInject constructor(private val repository: repository): ViewModel() { ... } //3. AndroidEntryPoint class HiltMvvmActivity: AppCompatActivity() { // @Inject // lateinit var viewModel: MyViewModel val viewModel: MyViewModel by viewModels() MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) } ... }Copy the code
SavedStateHandle and @assist annotations
  • There are three cases when an Activity/Fragment is destroyed:
    1. The application is closed or exited
    2. The Activity configuration is changed, for example when the screen is rotated;
    3. In the background due to insufficient running memory is recovered by the system;
  • The ViewModel handles case 2, while case 3 requires onSaveInstanceState() to save the data and SavedStateHandle to restore the data when rebuilding, adding the SavedStateHandle dependency with the @Assist annotation
class MyViewModel @ViewModelInject constructor( private val repository: Repository, //SavedStateHandle Saves and recovers data when a process is terminated @Assisted Private Val SavedStateHandle: SavedStateHandle) : ViewModel() { var result: MutableLiveData<String> = MutableLiveData() private val userId: MutableLiveData<String> = savedStateHandle.getLiveData("userId") fun doWork() { viewModelScope.launch { runCatching { withContext(Dispatchers.IO) { repository.getData(userId) } }.onSuccess { result.value = "RepoResponse=${Gson().toJson(it)}" }.onFailure { result.value = it.message } } } }Copy the code

Inject dependencies into classes not supported by Hilt

  • You can create entry points using the @entrypoint annotation and call the static method of EntryPointAccessors to get an instance of a custom EntryPoint
  • EntryPointAccessors provides four static methods: FromActivity, fromApplication, fromFragment, fromView, according to the custom entry MyEntryPoint annotation @installin specified scope to select the corresponding access method;
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint{
    fun getRetrofit():Retrofit
}
Copy the code
Used in ContentProvider
  • One key Android component is missing from Hilt’s support entry point: ContentProvider, the main reason is that contentProvider.oncreate () is executed before onCreate() of the Application, App Startup. Hilt works from application.oncreate (). None of Hilt’s functions work properly until contentProvider.oncreate () is executed.
class MyContentProvider : ContentProvider() { override fun onCreate(): Boolean { context? . Let {val appContext = it. ApplicationContext EntryPointAccessors. / / call fromApplication () function to get the custom entry point instances of val entryPoint=EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java) Val Retrofit = entryPoint.getretrofit () ljylogutil.d (" Retrofit :$Retrofit ")} return true } ... }Copy the code
Used in App Startup
  • App Startup provides an InitializationProvider by default. InitializationProvider inherits ContentProvider.
class LjyInitializer : Initializer<Unit> { override fun create(context: Context) {/ / call EntryPointAccessors fromApplication () function to get the custom entry point instances of val entryPoint = EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java) // call the getRetrofit() function defined in the entryPoint to get an instance of Retrofit. Val Retrofit = entrypoint.getretrofit () LjyLogUtil.d("retrofit:$retrofit") } override fun dependencies(): List<Class<out Initializer<*>>> { return emptyList() } }Copy the code

An error to solve

  • Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}
//If so, try changing from "arguments =" to "arguments +=", as just using equals overwrites anything set previously.
Copy the code

My name is Jinyang. If you want to learn more about jinyang, please pay attention to the wechat public number “Jinyang said” to receive my latest articles