相关知识
依赖注入
- 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()
    }
}
复制代码
- 或是使用Room操作本地数据库
 
@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<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层
@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<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
            }
        }
    }
}
复制代码
在 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<Unit> {
    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<Class<out Initializer<*>>> {
        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.
复制代码
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
    


















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)