Android Jetpack架构组件(八)— Hilt

一、Hilt简介

Hilt Google开源的一个 Android 的依赖注入库,其实是基于 Dagger。
Hilt 是专门为android打造的 ,可以使我们的代码 尽量的简化
Hilt 创建了一组标准的 组件和作用域。这些组件会自动集成到 Android 程序中的生命周期中。在使用的时候可以指定使用的范围,事件作用在对应的生命周期当中。

1、依赖注入

Dependency Injection,简称DI;
作用:使代码解耦,便于复用,重构和测试

类通常需要引用其他类,可通过以下三种方式获取所需的对象:

  1. 在类中创建所需依赖项的实例
  2. 通过父类或其他类获取
  3. 以参数形式提供,可以在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数

第三种方式就是依赖项注入

2、Android中的依赖注入方式

1.手动依赖注入
(1)、构造函数注入
//在构造类时提供这些依赖项
class Car (private val driver: Driver) {
    fun run() {
        driver.run()
    }
}
val driver = Driver()
val car= Car(driver)
car.run()
(2)、字段注入(或 setter 注入)
//将依赖项传入需要依赖项的函数
class Car {
    lateinit var driver: Driver
    fun run() = driver.run()
}
val car = Car()
val driver = Driver()
car.driver = driver
car.use()

上面两种都是手动依赖项注入,但是如果依赖项和类过多,手动依赖注入就会产生一些问题:

1. 使用越来越繁琐;
2. 产生大量模板代码;
3. 必须按顺序声明依赖项;
4. 很难重复使用对象;
2.自动依赖注入框架

有一些库通过自动执行创建和提供依赖项的过程解决此问题,实现原理有如下几种方案:

1. 通过反射,在运行时连接依赖项;
2. 通过注解,编译时生成连接依赖项的代码;
3. kotlin 强大的语法糖和函数式编程;
(1)、Dagger

Android领域最广为熟知的依赖注入框架

  • Dagger 1.x版本:
    Square基于反射实现的,有两个缺点一个是反射的耗时,另一个是反射是运行时的,编译期不会报错。而使用难度较高,刚接触时有经常容易写错,造成开发效率底;
  • Dagger 2.x版本:
    Google基于Java注解实现的,完美解决了上述问题,
(2)、Koin

为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射(通过kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程实现);

(3)、Hilt

由于Dagger的复杂度和使用难度较大,Android团队联合Dagger2团队,一起开发出来的一个专门面向Android的依赖注入框架Hilt,最明显的特征就是:1. 简单;2. 提供了Android专属的API;3. Google官方支持,和Jetpack其他组件配合使用;

Hilt 通过为项目中的每个 Android 类提供容器并自动为您管理其生命周期,定义了一种在应用中执行 DI 的标准方法。
Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。

二、Hilt的使用

1、依赖版本配置

项目根build.gradle依赖

ext.kotlin_version = "1.5.0"//kotlin版本
ext.hilt_version = '2.35'//hilt版本
repositories {
    google()
    mavenCentral()
}
dependencies {
    classpath "com.android.tools.build:gradle:4.2.1"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35'
}

项目app中build.gradle依赖

implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

项目模块中build.gradle所使用插件

id 'kotlin-kapt'
id 'kotlin-android-extensions'

2、Hilt应用

1.定义Hilt应用类
  • 用@HiltAndroidApp注解Application;所有使用Hilt注入框架的应用都必须包含一个带有 @HiltAndroidApp 注释的 Application 类。
  • @HiltAndroidApp注解 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器;
@HiltAndroidApp
class MyApplication : MultiDexApplication() {

}

然后,在AndroidManifest.xml中引入我们自定义的Application类。此时,生成的Hilt组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外,由于它也是应用的父组件,所以其他组件可以访问它提供的依赖项。

2.创建注入类

为了执行字段注入,需要在类的构造函数中使用 @Inject 注解,以告知 Hilt 如何提供该类的实例

class Car @Inject constructor() {
    var name: String = ""

    fun run() {
    }
}

使用 @Inject 注释执行字段
@AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项, 如需从组件获取依赖项,请使用 @Inject 注释执行字段注入, 注意:Hilt注入的字段是不可以声明成private的;

3.依赖项注入 Android 类

用@AndroidEntryPoint注释类
目前支持6类入口点:Application(通过使用 @HiltAndroidApp),Activity,Fragment,View,Service,BroadcastReceiver

使用 @AndroidEntryPoint 注解 Android 类,还必须为依赖于该类的 Android 类添加注释,例如为注解 fragment ,则还必须为该 fragment 依赖的 Activity 添加@AndroidEntryPoint注释。

@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
    @Inject
    lateinit var car: Car
}

注意:带参数的依赖注入,需要构造函数中所依赖的所有其他对象都支持依赖注入;例如:Car的构造函数有Engine,则Engine构造函数也需要@Inject。

3、Hilt模块

有时一些类型参数(如接口或来自外部库的类)不能通过构造函数注入,对于这种情况,我们可以使用Hilt 模块来向Hilt提供绑定信息。

Hilt模块是一个带有@Module注释的类,并使用 @InstallIn 设置作用域。与Dagger模块的作用一样,它会告知Hilt如何提供某些类型的实例。与Dagger模块不同的是,我们必须使用@InstallIn注解为Hilt模块添加注释,以告知Hilt模块将用在哪个Android类中。

1.@Binds注入接口

由于Phone是一个接口,无法通过构造函数注入Hilt,而应向Hilt提供绑定信息。即在Hilt模块内创建一个带有@Binds注释的抽象函数。

通常,带有注释的函数会向Hilt提供以下信息:

  1. 函数返回类型会告知Hilt函数提供的是哪个接口的实例。
  2. 函数参数会告知Hilt需要提供哪种实现。
//1. 接口
interface Phone {
    fun call()
}
//2. 实现类
class Huawei @Inject constructor() : Phone {
    override fun call() {
    }
}
//3. 被注入的类,入参是接口类型
class People @Inject constructor(val phone: Phone) {
    fun call() {
        phone.call()
    }
}
//4. 使用@Binds注入接口实例
@Module
@InstallIn(ActivityComponent::class)
abstract class PhoneModel {
    @Binds
    abstract fun bindPhone(phone: Huawei): Phone
}
//5. 使用注入的实例
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
    @Inject
    lateinit var people: People

    fun test() {
        people.phone.call()
    }
}

由于PhoneModel 带有 @InstallIn(ActivityComponent.class) 注释,因为我们可以将该依赖项注入Activity中。并且,PhoneModel中的所有依赖项都可以在所有Activity中使用。

2.@Provides注入实例

如果某个类不归您所有(因为它来自外部库,如 Retrofit、OkHttpClient 或 Room 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。

这种情况注入方法是在Hilt模块内创建一个函数,然后使用@Provides为该函数添加注释。

带有注释的函数会向 Hilt 提供以下信息:

  1. 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
  2. 函数参数会告知 Hilt 相应类型的依赖项。
  3. 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。
@Module
@InstallIn(SingletonComponent::class)
class DINetworkModule {

    /**
     * [OkHttpClient]依赖提供方法
     *
     * @return OkHttpClient
     */
    @Singleton
    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        // 日志拦截器部分
        val level = if (BuildConfig.VERSION_TYPE != VersionStatus.RELEASE) BODY else NONE
        val logInterceptor = HttpLoggingInterceptor().setLevel(level)

        return OkHttpClient.Builder()
            .retryOnConnectionFailure(true)
            .connectTimeout(15L * 1000L, TimeUnit.MILLISECONDS)
            .readTimeout(20L * 1000L, TimeUnit.MILLISECONDS)
            .writeTimeout(20L * 1000L, TimeUnit.MILLISECONDS)
            .addInterceptor(logInterceptor)
//            .addInterceptor(CookiesInterceptor())
            .build()
    }

    /**
     * 项目主要服务器地址的[Retrofit]依赖提供方法
     *
     * @param okHttpClient OkHttpClient OkHttp客户端
     * @return Retrofit
     */
    @Singleton
    @Provides
    fun provideMainRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(HttpBaseUrlConstant.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()
    }
}
3.限定符
  • 限定符也是一种注解,当为某个类型定义了多个绑定时,我们可以使用它来标识该类型的特定绑定。
  • 使用@Qualifier注解实现限定符。
3.1为同一类型提供多个绑定

如果需要让Hilt以依赖项的形式提供同一类型的不同实现,那么必须向 Hilt 提供多个绑定,同一类型定义多个绑定可以使用限定符来实现。

//1. 接口和实现类
interface Phone {
    fun call()
}

class Huawei @Inject constructor() : Phone {
    override fun call() {
    }
}

class Xiaomi @Inject constructor() : Phone {
    override fun call() {
    }
}

//2. 创建多个类型的注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindHuawei

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

//@Retention:注解的生命周期
//AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中
//AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见
//AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见


//3. 在Hilt模块中使用注解
@Module
@InstallIn(ActivityComponent::class)
abstract class PhoneModel {
    @BindHuawei
    @Binds
    abstract fun bindHuawei(cpu: Huawei): Phone

    @ BindXiaomi
    @Binds
    abstract fun bindXiaomi(cpu: Xiaomi): Phone
}

//4. 使用依赖注入获取实例,可以用在字段注解,也可以用在构造函数或者方法入参中
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
    @BindHuawei
    @Inject
    lateinit var huawei: Phone

    @BindXiaomi
    @Inject
    lateinit var xiaomi: Phone

    fun use() {
        huawei.call()
        xiaomi.call()
    }
}

如果需要向某个类型添加限定符,那么应该向提供该依赖项的所有可能的渠道都添加限定符。这是因为让基本的实现或通用的实现在不带限定符的情况下避免出错,也是为了避免导致Hilt注入错误的依赖项。

3.2Hilt中的预定义限定符

Hilt提供了一些预定义的限定符。
Hilt 提供的@ApplicationContext 和 @ActivityContext 限定符用来获取Context 类

class ModuleOne @Inject constructor(@ApplicationContext private val context: Context)

class ModuleTwo @Inject constructor(@ActivityContext private val context: Context)

对于Application和Activity这两个类型,Hilt也是给它们预置好了注入功能(必须是这两个,即使子类也不可以)

class ModuleOne @Inject constructor(val application: Application)

class ModuleTwo @Inject constructor(val activity: Activity)

4、Hilt内置组件类型

@Module:将这个类注册为module
@InstallIn():把这个模块安装到哪个组件中

Hilt内置了7种组件可选:
  1. SingletonComponent(ApplicationComponent废弃):
    对应Application,依赖注入实例可以在全项目中使用
  2. ActivityRetainedComponent:
    对应ViewModel(在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁)
  3. ActivityComponent:
    对应Activity,Activity中包含的Fragment和View也可以使用;
  4. FragmentComponent:
    对应Fragment
  5. ViewComponent:
    对应View
  6. ViewWithFragmentComponent:
    对应带有 @WithFragmentBindings 注释的 View
  7. ServiceComponent:
    对应Service
Hilt内置7种组件作用域注解:
  1. @Singleton:
    对应组件ApplicationComponent,整个项目共享同一个实例
  2. @ActivityRetainedScope:
    对应组件ActivityRetainedComponent
  3. @ActivityScoped:
    对应组件ActivityComponent,在同一个Activity(包括其包含的Fragment和View中)内部将会共享同一个实例
  4. @FragmentScoped:
    对应组件FragmentComponent
  5. @ViewScoped:
    对应组件ViewComponent和ViewWithFragmentComponent;
  6. @ServiceScopedService:
    对应ServiceComponent
组件的层次结构。

将Hilt模块安装到组件后,其绑定就可以与其他绑定的依赖项相互发生作用了,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项。


组件的层次结构
组件

注意:ApplicationComponent废弃,改为SingletonComponent

5、ViewModel的依赖注入

ViewModel中创建Repository并在Activity中使用

//1. 仓库层
class Repository @Inject constructor(){
    @Inject
    lateinit var apiService: ApiService
    suspend fun requestInfo(): RepoResponse {
        return apiService.requestInfo()
    }
}
//2. ViewModel层
@ActivityRetainedScoped
class MyViewModel @Inject constructor(private val repository: Repository): ViewModel() {
    var ld_result: MutableLiveData = MutableLiveData()
    fun requestInfo() {
        viewModelScope.launch {
            runCatching {
                withContext(Dispatchers.IO){
                    repository. requestInfo()
                }
            }.onSuccess {
                ld_result.value=gson().toJson(it).toString()
            }.onFailure {
                ld_result.value=it.message
            }
        }
    }
}
//3. Activity层
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hilt)
        viewModel.ld_result.observe(this, Observer {

        })
        viewModel. requestInfo()
    }
}
@HiltViewModel注解ViewModel

Hilt专门为其提供了一种独立的依赖注入方式: @HiltViewModel来获取ViewModel实例。

//1. 仓库层
class CommonRepository @Inject constructor() : BaseRepository() {

    @Inject
    lateinit var mApi: HomeApiService

    suspend fun requestHome(
        callback: RequestCallback
    ) = request(callback) { mApi.requestHome() }

}

//2. ViewModel层
@HiltViewModel
class HomeViewModel @Inject constructor(private val mRepository: CommonRepository) :
    BaseViewModel() {

}

//3. Activity层
@AndroidEntryPoint
class MainActivity : BaseActivity() {

    val mViewModel: HomeViewModel by viewModels()
//  或  val mViewModel: HomeViewModel by lazy { ViewModelProvider(this).get(HomeViewModel::class.java) }
 
}
SavedStateHandle 和 @assist注解

Activity/Fragment被销毁一般有三种情况:

  1. 界面关闭或退出应用
  2. Activity 配置 (configuration) 被改变,如旋转屏幕时;
  3. 在后台时因运行内存不足被系统回收;

ViewModel 会处理2的情况,而3的情况就需要使用onSaveInstanceState()保存数据,重建时用SavedStateHandle恢复数据,就要用@assist 注解添加 SavedStateHandle 依赖项

@HiltViewModel
class HomeViewModel @Inject constructor(private val mRepository: CommonRepository,
    //SavedStateHandle 用于进程被终止时,保存和恢复数据
    @Assisted private val savedStateHandle: SavedStateHandle
) ) : BaseViewModel() {
    var ld_result: MutableLiveData = MutableLiveData()
    private val userId: MutableLiveData = savedStateHandle.getLiveData("userId")

    
}

6、Hilt不支持的注入依赖项

Hilt支持一些最常见的 Android 类,如Application、Activity、Fragment、View、Service和BroadcastReceiver。如果要在Hilt不支持的类中执行字段注入:

  • 可以使用 @EntryPoint 注释创建入口点, 调用EntryPointAccessors的静态方法来获得自定义入口点的实例,入口点是由Hilt管理的代码与并非由Hilt管理的代码之间的边界,它是代码首次进入Hilt所管理对象的图的位置,入口点允许Hilt使用它并不管理的代码提供依赖关系图中的依赖项。
  • EntryPointAccessors提供了四个静态方法:fromActivity、fromApplication、fromFragment、fromView,根据自定义入口的MyEntryPoint的注解@InstallIn所指定的范围选择对应的获取方法;
//1、注册创建入口点
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint{
    fun getRetrofit():Retrofit
}
//2、使用EntryPointAccessors获取实例
Context appContext = getContext().getApplicationContext();
MyEntryPoint hiltEntryPoint =EntryPointAccessors.fromApplication(appContext, MyEntryPoint.class);
Retrofit retrofit = hiltEntryPoint. getRetrofit();

参数是组件实例或充当组件持有者的 @AndroidEntryPoint 对象,使用时以参数形式传递的组件和 EntryPointAccessors 静态方法都与 @EntryPoint 接口上的 @InstallIn 注释中的 Android 类匹配。

在本例中,我们使用 ApplicationContext 作为检索入口点,因为入口点安装在 ApplicationComponent 中。如果需要检索的绑定位于 ActivityComponent 中那么改为 ActivityContext即可。

7、Hilt注入ContentProvider

  • Hilt支持的入口点中少了一个关键的Android组件:ContentProvider, 主要原因就是ContentProvider.onCreate() 在Application的onCreate() 之前执行,因此很多人会利用这个特性去进行提前初始化, 详见Android Jetpack系列--5. App Startup使用详解, 而Hilt的工作原理是从Application.onCreate()中开始的,即ContentProvider.onCreate()执行之前,Hilt的所有功能都还无法正常工作;
class MyContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        context?.let {
            val appContext=it.applicationContext
            //调用EntryPointAccessors.fromApplication()函数来获得自定义入口点的实例
            val entryPoint=EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java)
            //再调用入口点中定义的getRetrofit()函数就能得到Retrofit的实例
            val retrofit=entryPoint.getRetrofit()
        }
        return true
    }
}

8、Hilt注入Startup

App Startup 会默认提供一个 InitializationProvider,InitializationProvider 继承 ContentProvider;

class LjyInitializer : Initializer {
    override fun create(context: Context) {
        //调用EntryPointAccessors.fromApplication()函数来获得自定义入口点的实例
        val entryPoint= EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
        //再调用入口点中定义的getRetrofit()函数就能得到Retrofit的实例
        val retrofit=entryPoint.getRetrofit()
        LjyLogUtil.d("retrofit:$retrofit")
    }

    override fun dependencies(): List>> {
        return emptyList()
    }
}

报错解决:

Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

9、Hilt注入WorkManager

使用Hilt注入WorkManager,需要在Gradle文件中添加如下一些额外的依赖项

dependencies {
  implementation 'androidx.hilt:hilt-work:1.0.0-alpha01'
  // Kotlin.
  kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
  // Java.
  annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

首先,在Worker对象的构造函数中使用@WorkerInject注解来注入一个 Worker,然后在 Worker对象中使用@Singleton或未限定作用域的绑定对象的作用域,还必须使用 @Assisted 为 Context 和 WorkerParameters 依赖项添加注释。

public class ExampleWorker extends Worker {

  private final WorkerDependency workerDependency;

  @WorkerInject
  ExampleWorker(
    @Assisted @NonNull Context context,
    @Assisted @NonNull WorkerParameters params,
    WorkerDependency workerDependency
  ) {
    super(context, params);
    this.workerDependency = workerDependency;
  }
  ...
}

然后,让Application类实现Configuration.Provider接口,注入HiltWorkFactory 的实例,并将其传入WorkManager配置,如下所示。

@HiltAndroidApp
public class ExampleApplication extends Application implements Configuration.Provider {

  @Inject HiltWorkerFactory workerFactory;

  @Override
  public Configuration getWorkManagerConfiguration() {
    return Configuration.Builder()
             .setWorkerFactory(workerFactory)
             .build();
  }
}

参考:
Android Jetpack--Hilt使用详解
Android Jetpack架构组件(十二)之Hilt

你可能感兴趣的:(Android Jetpack架构组件(八)— Hilt)