依赖注入
- Dependency Injection,简称DI;
- 依赖项注入可以使代码解耦,便于复用,重构和测试
什么是依赖项注入
- 类通常需要引用其他类,可通过以下三种方式获取所需的对象:
- 在类中创建所需依赖项的实例
class CPU () {
var name: String = ""
fun run() {
LjyLogUtil.d("$name run...")
}
}
class Phone1 {
val cpu = CPU()
fun use() {
cpu.run()
}
}
- 通过父类或其他类获取
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCapabilities = cm.getNetworkCapabilities(cm.activeNetwork)
LjyLogUtil.d("是否有网络连接:${networkCapabilities==null}")
- 以参数形式提供,可以在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数;
Android中的依赖注入方式
手动依赖注入
1. 构造函数注入
//在构造类时提供这些依赖项
class Phone (private val cpu: CPU) {
fun use() {
cpu.run()
}
}
val cpu = CPU()
val phone=Phone(cpu)
phone.use()
2. 字段注入(或 setter 注入)
//将依赖项传入需要依赖项的函数
class Phone {
lateinit var cpu: CPU
fun use() = cpu.run()
}
val phone = Phone()
val cpu = CPU()
phone.cpu = cpu
phone.use()
- 上面两种都是手动依赖项注入,但是如果依赖项和类过多,手动依赖注入就会产生一些问题
1. 使用越来越繁琐;
2. 产生大量模板代码;
3. 必须按顺序声明依赖项;
4. 很难重复使用对象;
自动依赖注入框架
- 有一些库通过自动执行创建和提供依赖项的过程解决此问题,实现原理有如下几种方案:
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
- Hilt 通过为项目中的每个 Android 类提供容器并自动为您管理其生命周期,定义了一种在应用中执行 DI 的标准方法。
- Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
Hilt使用流程
添加依赖项
//1\. 配置Hilt的插件路径
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
//2\. 引入Hilt的插件
plugins {
...
id 'dagger.hilt.android.plugin'
id 'kotlin-kapt'
}
//3\. 添加Hilt的依赖库及Java8
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//对于 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"
}
Hilt 应用类
- 用@HiltAndroidApp注解Application;
- @HiltAndroidApp注解 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器;
@HiltAndroidApp
class MyApplication : MultiDexApplication() {
...
}
将依赖项注入 Android 类
1. 用@AndroidEntryPoint注释类;
- 目前支持6类入口点:Application(通过使用 @HiltAndroidApp),Activity,Fragment,View,Service,BroadcastReceiver
- 使用 @AndroidEntryPoint 注解 Android 类,还必须为依赖于该类的 Android 类添加注释,例如为注解 fragment ,则还必须为该 fragment 依赖的 Activity 添加@AndroidEntryPoint注释。
2. 使用 @Inject 注释执行字段
- @AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项, 如需从组件获取依赖项,请使用 @Inject 注释执行字段注入, 注意:Hilt注入的字段是不可以声明成private的;
3. 构造函数中使用 @Inject 注释
- 为了执行字段注入,需要在类的构造函数中使用 @Inject 注释,以告知 Hilt 如何提供该类的实例:
@AndroidEntryPoint
class HiltDemoActivity : AppCompatActivity() {
@Inject
lateinit var cpu: CPU
...
}
class CPU @Inject constructor() {
var name: String = ""
fun run() {
LjyLogUtil.d("$name run...")
}
}
带参数的依赖注入:
- 如果构造函数中带有参数,Hilt要如何进行依赖注入呢?
- 需要构造函数中所依赖的所有其他对象都支持依赖注入
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 = "麒麟990"
phone.use()
}
}
Hilt Module
- 有时一些类型参数不能通过构造函数注入, 如 接口 或 来自外部库的类,此时可以使用 Hilt模块 向Hilt提供绑定信息;
- Hilt 模块是一个带有 @Module 注释的类,并使用 @InstallIn 设置作用域
使用 @Binds 注入接口实例
//1\. 接口
interface ICPU {
fun run()
}
//2\. 实现类
class KylinCPU @Inject constructor() : ICPU {
override fun run() {
LjyLogUtil.d("kylin run...")
}
}
//3\. 被注入的类,入参是接口类型
class Phone @Inject constructor(val cpu: ICPU) {
fun use() {
cpu.run()
}
}
//4\. 使用@Binds注入接口实例
@Module
@InstallIn(ActivityComponent::class)
abstract class CPUModel {
@Binds
abstract fun bindCPU(cpu: KylinCPU): ICPU
}
//5\. 使用注入的实例
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject
lateinit var phone: Phone
fun test() {
phone.cpu.name = "麒麟990"
phone.use()
}
}
使用 @Provides 注入实例
- 如果某个类不归您所有(因为它来自外部库,如 Retrofit、OkHttpClient 或 Room 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。
@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()
}
}
为同一类型提供多个绑定
- 比如网络请求中可能需要不同配置的OkHttpClient,或者不同BaseUrl的Retrofit
- 使用@Qualifier注解实现
//1\. 接口和实现类
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\. 创建多个类型的注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindKylinCPU
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindSnapdragonCPU
//@Retention:注解的生命周期
//AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中
//AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见
//AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见
//3\. 在Hilt模块中使用注解
@Module
@InstallIn(ActivityComponent::class)
abstract class CPUModel {
@BindKylinCPU
@Binds
abstract fun 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()
}
}
组件默认绑定
- 由于可能需要来自 Application 或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
- 每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定
class Test1 @Inject constructor(@ApplicationContext private val context: Context)
class Test2 @Inject constructor(@ActivityContext private val context: Context)
- 对于Application和Activity这两个类型,Hilt也是给它们预置好了注入功能(必须是这两个,即使子类也不可以)
class Test3 @Inject constructor(val application: Application)
class Test4 @Inject constructor(val activity: Activity)
Hilt内置组件类型
- 上面使用Hilt Module时,有用到@InstallIn(), 意思是把这个模块安装到哪个组件中
Hilt内置了7种组件可选:
- ApplicationComponent:对应Application,依赖注入实例可以在全项目中使用
- ActivityRetainedComponent:对应ViewModel(在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁)
- ActivityComponent:对应Activity,Activity中包含的Fragment和View也可以使用;
- FragmentComponent:对应Fragment
- ViewComponent:对应View
- ViewWithFragmentComponent:对应带有 @WithFragmentBindings 注释的 View
- ServiceComponent:对应Service
- Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接从 ApplicationComponent 注入 broadcast receivers;
组件作用域
- Hilt默认会为每次的依赖注入行为都创建不同的实例。
Hilt内置7种组件作用域注解
- @Singleton:对应组件ApplicationComponent,整个项目共享同一个实例
- @ActivityRetainedScope:对应组件ActivityRetainedComponent
- @ActivityScoped:对应组件ActivityComponent,在同一个Activity(包括其包含的Fragment和View中)内部将会共享同一个实例
- @FragmentScoped:对应组件FragmentComponent
- @ViewScoped:对应组件ViewComponent和ViewWithFragmentComponent;
- @ServiceScopedService:对应ServiceComponent
- 比如我们经常会需要一个全局的OkhttpClient或者Retrofit,就可以如下实现
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)
}
//组件作用域:Hilt默认会为每次的依赖注入行为都创建不同的实例。
@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()
}
}
@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() //允许在主线程中查询
.build()
}
@Provides
@Singleton
fun provideRepoDao(appDatabase: AppDatabase):RepoDao{
return appDatabase.repoDao()
}
}
ViewModel的依赖注入
- 通过上面的学习,那我们如果在ViewModel中创建Repository要如何实现呢,可以如下:
//1\. 仓库层
class Repository @Inject constructor(){
@Inject
lateinit var apiService: ApiService
suspend fun getData(): RepoResponse {
return apiService.searRepos(1, 5)
}
}
//2\. ViewModel层
@ActivityRetainedScoped
class MyViewModel @Inject constructor(private val repository: Repository): ViewModel() {
var result: MutableLiveData = 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层
@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()
}
}
ViewModel 和 @ViewModelInject 注解
- 这种改变了获取ViewModel实例的常规方式, 为此Hilt专门为其提供了一种独立的依赖注入方式: @ViewModelInject
//1\. 添加两个额外的依赖
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
//2\. 修改MyViewModel: 去掉@ActivityRetainedScoped注解,把@Inject改为@ViewModelInject
class MyViewModel @ViewModelInject constructor(private val repository: Repository): ViewModel() {
...
}
//3\. activity中viewModel获取改为常规写法
@AndroidEntryPoint
class HiltMvvmActivity : AppCompatActivity() {
// @Inject
// lateinit var viewModel: MyViewModel
val viewModel: MyViewModel by viewModels()
// 或 val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
...
}
SavedStateHandle 和 @assist注解
- Activity/Fragment被销毁一般有三种情况:
- 界面关闭或退出应用
- Activity 配置 (configuration) 被改变,如旋转屏幕时;
- 在后台时因运行内存不足被系统回收;
- ViewModel 会处理2的情况,而3的情况就需要使用onSaveInstanceState()保存数据,重建时用SavedStateHandle恢复数据,就要用@assist 注解添加 SavedStateHandle 依赖项
class MyViewModel @ViewModelInject constructor(
private val repository: Repository,
//SavedStateHandle 用于进程被终止时,保存和恢复数据
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var result: MutableLiveData = MutableLiveData()
private val userId: MutableLiveData = 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
}
}
}
}
在 Hilt 不支持的类中注入依赖项
- 可以使用 @EntryPoint 注释创建入口点, 调用EntryPointAccessors的静态方法来获得自定义入口点的实例
- EntryPointAccessors提供了四个静态方法:fromActivity、fromApplication、fromFragment、fromView,根据自定义入口的MyEntryPoint的注解@InstallIn所指定的范围选择对应的获取方法;
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint{
fun getRetrofit():Retrofit
}
在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()
LjyLogUtil.d("retrofit:$retrofit")
}
return true
}
...
}
在 App 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()]
}
}
}
}
//If so, try changing from "arguments =" to "arguments +=", as just using equals overwrites anything set previously.