Jetpack-依赖注入(Dagger)

什么是依赖注入

在阅读别人得项目代码时,经常会看到某一个成员变量或者构造函数的入参上被添加@Injcet注解,如果没有了解过依赖注入框架,此时都会很迷惑,这个注解得作用是什么,接下来就简单带大家了解一下,依赖注入以及Dagger2的作用。

官方解释

依赖项注入 (DI) 是一种在编程中广泛应用的技术,非常适合用于 Android 开发。遵循 DI 的原则可以为构造良好的应用架构奠定基础。

实现依赖项注入可为您带来以下优势:

  • 重用代码
  • 易于重构
  • 易于测试

手动依赖注入

把类的实例传递给另一个对象得过程,就叫做依赖注入。

栗子:

在MVP架构中,我们有一个Presenter类,他通常需要持有Model的引用, 这其实就是把Model 注入给了Presenter层,这便是手动依赖注入。

Kotlin 复制代码
    class Presenter(model:Model){
        private val model= Model()
    }

    class Model(){
        fun getDataForNet(){
            //do....
        }
    }

定义

自行创建、提供并管理不同类的依赖项,而不依赖于库。这称为手动依赖项注入或人工依赖项注入。

日常开发中,类通常通过以下三种方式获取所需的对象:

scss 复制代码
1.类构造其所需的依赖项。例如 `private val model= Model()`
2.从其他地方抓取。某些 Android API(如 Context getter 和 getSystemService())的工作原理便是如此。
3.以参数形式提供。class Presenter(model:Model){}

整个过程,由两方完成,需求方被注入方

自动依赖注入

在编译时期,为被注入方生成实例,通过容器接口生成的容器类进行管理

为什么需要自动依赖注入,明明一个new Model() 就能解决的问题,为什么还要单独设计个框架去做这件事呢?

我们增加一下MVP的难度,P层中 除了M层的引用,还需要做分页管理 PageLoader(),数据类型转换DataConvert()等等,由于业务和需求变大,我们可能需要维护很多对象,尤其是那些对象依赖层级较深的,不仅不便于管理,还会使项目耦合性变大,导致不易懂,当修改中间层引用时,导致耦合性变大,容易出现问题。

Dagger2介绍

官方释义

Dagger2是一个依赖注入框架,由Google维护,用于在Android中实现依赖注入。 Dagger 可以执行以下操作,使您无需再编写冗长乏味又容易出错的样板代码:

  • 生成您在手动 DI 部分手动实现的 AppContainer 代码(应用图)。
  • 为应用图中提供的类创建 factory。这就是在内部满足依赖关系的方式。
  • 重复使用依赖项或创建类型的新实例,具体取决于您如何使用作用域配置该类型。
  • 为特定流程创建容器,操作方法与上一部分中使用 Dagger 子组件为登录流程创建容器的方法相同。这样可以释放内存中不再需要的对象,从而提升应用性能。 只要您声明类的依赖项并指定如何使用注释满足它们的依赖关系,Dagger 便会在构建时自动执行以上所有操作。Dagger 生成的代码与您手动编写的代码类似。在内部,Dagger 会创建一个对象图,然后它可以参考该图来找到提供类实例的方式。对于图中的每个类,Dagger 都会生成一个 factory 类型类,它会使用该类在内部获取该类型的实例。

优点

  1. 基于注解的依赖配置,大大简化了依赖注入的使用和配置。开发者只需要使用@Inject、@Module、@Component等注解,即可声明依赖和关系,而不需要写任何代码来执行依赖注入。
  2. 运行期动态注入,提高了性能。Dagger2会在编译期生成代码,在运行期高效执行依赖注入,而不是使用反射,因此性能很高。
  3. 支持懒加载,按需创建。可以只在真正需要时创建实例,减少内存占用。
  4. 支持组件作用域的依赖注入,更加灵活。可以自定义组件的作用域,实现不同组件间的隔离。
  5. 完全静态、可预测、无运行时开销。编译期生成代码,不存在反射和动态代理开销,做到性能最优。
  6. 内置生命周期和多绑定支持,使用方便。

个人理解


Dagger2用于管理项目中的实例 ,通过注解和Javapot 在编译时生成被注入方的实例模板,生成容器类,用于需求方被注入方的关联绑定。

通常两种依赖注入方式:

  • 构造
  • 变量

Dagger2的使用

项目引入依赖

Java 复制代码
implementation 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'

记得引入 kapt的插件

ini 复制代码
 如果不自动生成代码,记得更新下gradle.properties 的配置 kapt.incremental.apt = false 后 rebulid一下

基础使用

假设开过过程中,有一个数据仓库类,数据来源于本地和网络,TestRepository 在创建时需要 TestRemoteDataSourceTestLocalDataSource对象,通过在TestRepository(需要注入的对象)和 TestRemoteDataSource(被注入的对象)的构造上添加@Inject注解,通知Dagger,就会自动创建实例

Java 复制代码
//容器类,用于和需求方关联
@Component
interface TestContainer {
    fun repository(): TestRepository
}
// 助需要注入的类
class TestLocalDataSource @Inject constructor() {
}
class TestRemoteDataSource @Inject constructor()  {
}
// 构造注入
class TestRepository @Inject constructor(
val testRemoteDataSource: TestRemoteDataSource,
val testLocalDataSource: TestLocalDataSource) {}
// 在需要全局提供的对象的构造参数上 加上 @Inject 在全局提供的地方加上@Component  ,
// build 模块 ,自动生成Dagger+ 接口名称
bash 复制代码
build项目后,自动生成的容器/需求方的实例工厂类,在 app>bulid>source>kapt>debug 下面
@Inject 构造方法

@Inject通常用在构造方法类成员变量

当被@Inject 修饰的构造需要参数时,参数本身的构造也必须被@Inject,为了创建关联,自动生成依赖。

被@Inject修饰的类构造没有参数时,自动生成了一个该类的工厂类,用于提供该类的实例和工厂本身的单例

被@Inject修饰的类构造有参数时,自动生成了一个该类的工厂类,根据参数,通过参数的工厂类,获取对应的对象。

@Component

@Component用在接口上,用于定义一个Component。

我们这时候只需通过Dagger+容器名,就可以获取到需要的对象实例

Kotlin 复制代码
 //调用
 val  testRepository =  DaggerTestContainer.builder().build().repository()
@Inject 类成员变量

除了构造方法以外,@Inject还可以修饰成员变量

Kotlin 复制代码
@Inject
lateinit var testRepository: TestRepository

还需要在TestContainer接口里面加一个方法

Kotlin 复制代码
@Component
interface TestContainer {
  //mainActivity = 需求方
  fun inject(mainActivity: MainActivity)
}

除了@Inject 本身生成的类,@Component容器里面也会生成一个新的方法

Kotlin 复制代码
 //调用
 DaggerTestContainer.create().inject(MainActivity.this)
@Module

日常开发中,我们会经常会使用第三方SDK,例如OkhttpClient进行网络数据请求,通常这种情况,我们是没办法对其构造进行@Inject的,那这时候,我们就需要通过@Module 进行实例的注入

Java 复制代码
// 1 为Container创建 一个module
// 通过@Provides 可以提供多个实例
@Module
class BaseModule constructor(var context: Context){
    @Provides
    fun getOkhttpClient(): OkhttpClient {
        return OkHttpClient.Builder().build()
    }

    @Provides
    fun....
}
// 2 为Container绑定Module
@Component(modules = [BaseModule::class])
interface BaseContainer {
    //提供一个接收注入类的实例
    fun inject(baseActivity: BaseActivity)
}

class MainActivity : AppCompatActivity() {
  @Inject  
  lateinit var okHttpClient: OkHttpClient

// 3 调用
DaggerBaseContainer.builder().baseModule(BaseModule(baseContext)).build().inject(this)
}

注:如果需求方没有@Inject属性, baseModule()会显示被作废的方法,无法关联

容器类

使用@Module + @Provides 会生成工厂类,专门生产@Provides所注释方法返回的实例,如果有多个Module 或者 Provides,则生产多个工厂类。

@Singleton

对于上面的OkhttpClient而言,在没有特殊情况下,项目中始终需要唯一配置好的Client,那这时候,我们就需要使用Singleton来修饰module中的@Provides方法。

@Singleton 用于修饰容器接口 和 被@Provides的方法。

注:如果模块中 的@Provides 中使用了@Singleton,容器类也要标记

@Qualifier

正常开发中,除了我们本身项目的服务以外,我们可能还需要调用一些第三方的api,例如百度的服务。

如果在一个类里面同时需要两个OkhttpClient 对应不同的服务,一个Module 是无法做到的,那我们如何区分呢?通过@Qualifier注解。

kotlin 复制代码
//修饰注解
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DevOkhttpClient

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class BaiDuOkhttpClient
kotlin 复制代码
@Module
class BaseModule constructor(var context: Context) {
    //修饰Provides
    @Singleton
    @Provides
    @DevOkhttpClient
    fun devOkhttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
    
    @BaiDuOkhttpClient
    @Singleton
    @Provides
    fun baiDuOkhttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}
less 复制代码
    @Inject
    @DevOkhttpClient
    lateinit var devOkHttpClient: OkHttpClient

    @BaiDuOkhttpClient
    @Inject
    lateinit var baiDuOkHttpClient: OkHttpClient
    ....
    //绑定
    DaggerBaseContainer.builder().baseModule(BaseModule(baseContext)).build().inject(this)

Dagger中模块的关系

上面我们描述了几种开发中的场景,对于实际的项目中,类与类之间的关联,会更为复杂。

所以我们需要构建更为复杂的组织关联。

相对于的类的特性,容器管理模块时,也有继承,依赖关系。

@dependencies

作用:

  1. 提供依赖关系: dependencies 用于指定一个组件依赖于另一个组件或者容器,使得依赖的组件可以访问被依赖组件提供的依赖对象。
  2. 组件组合: 允许在一个应用中组合多个组件,每个组件负责管理不同的依赖。这有助于模块化和清晰地定义应用的不同部分。

例子:我们经常会在Application 中进行一些SDK的初始化,我们需要在不同的页面使用这些SDK,此时,我们可以创建一个关系图谱
AppModule

kotlin 复制代码
@Module
class AppModule {

    @Provides
    fun proAppModule(): AppData {
        return AppData("AppData")
    }

}

data class AppData(val name: String)

ActivityModule

kotlin 复制代码
@Module
class ActivityModule {

    @Provides
    fun proActivityData(): ActivityData {
        return ActivityData("ActivityData")
    }

}
data class ActivityData(val name: String)

容器类

kotlin 复制代码
@Component(modules = [AppModule::class])
interface AppComponent {
    fun provideSomeDependency(): AppData
}

@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {
    fun provideSomeOtherDependency(mainActivity: MainActivity)

    fun provideSomeDependency(): AppData 
}

MyApp 需求方

kotlin 复制代码
class MyApp : Application() {

    val appComponent: AppComponent by lazy {
        DaggerAppComponent.create()
    }
}

最终需求方Activity

KOTLIN 复制代码
class MainActivity : AppCompatActivity() {
    lateinit var activityData: ActivityData
    @Inject
    lateinit var appData: AppData

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DaggerActivityComponent
            .builder()
            .appComponent((application as MyApp).appComponent)
            .build()
            .provideSomeOtherDependency(this)
        println("appData ${appData.name}" )  // AppData
        println("activityData ${activityData.name}" ) // activityData
    }

}

通过 依赖关系,在Activity中不仅可以拿到本身的依赖,也可以拿到全局的配置 注:依赖关系 属于作用域分配,模块中 不用使用了@Singleton,会冲突

@Subcomponent

作用: 子组件是继承并扩展父组件的对象图的组件。

上面的例子是由全局到局部,Subcomponent由局部到全局。 具体例子可以看一下Android开发者官网,有一个详细Demo的解释

区别

  1. 创建方式:

    • dependencies 通常用于描述独立的组件之间的依赖关系,这些组件通常位于不同的模块或者不同的层次结构中。
    • subcomponent 通常用于描述父子组件之间的依赖关系,通常用于创建具有相同作用域的相关组件集合,用于控制生命周期。
  2. 依赖关系类型:

    • dependencies 用于描述两个独立的组件之间的依赖关系。一个组件可以通过 dependencies 关键字声明它依赖于另一个组件,从而获取其他组件提供的依赖项。
    • subcomponent 用于描述父子组件之间的依赖关系。子组件会继承父组件的依赖项,同时可以定义自己的额外依赖项。
  3. 实例化方式:

    • dependencies 用于通过 @Component 注解的 dependencies 属性来声明依赖关系,然后通过 builder() 方法或者 create() 方法来实例化依赖组件。
    • subcomponent 则通过在父组件中定义一个方法来提供子组件的实例,然后通过该方法来获取子组件的实例。
  4. 作用域:

    • 通过 dependencies 声明的依赖关系不会传递作用域。即使父组件和子组件具有相同的作用域,它们之间的依赖关系也不会传递作用域。
    • subcomponent 中的依赖关系会继承父组件的作用域。这意味着子组件中提供的依赖项会具有与父组件相同的作用域,从而确保依赖项在整个组件层次结构中的生命周期一致。

总结

虽然理解起来,依赖注入会让人觉得开发繁琐,经常编译错误,但真的理解并掌握他的时候,你会发现,实例的管理会更加简单,代码的可维护性更易理解、测试和扩展会更加方便。

参考资料

相关推荐
m0_5287238117 分钟前
vue2与vue3的区别
前端·javascript·vue.js
huangfuyk33 分钟前
Vue3+Element Plus:使用el-dialog,对话框可拖动,且对话框弹出时仍然能够在背景页(对话框外部的页面部分)上进行滚动以及输入框输入信息
前端·javascript·vue.js·vue 3
突然好热39 分钟前
cesium效果不酷炫怎么办--增加渲染器
开发语言·前端·javascript
yery1 小时前
Ubuntu24.04中安装Electron
前端·javascript·electron
小夏同学呀2 小时前
使用elementplus中的分页器,后端一次性返100条数据,前端自己做分页处理,vue3写法
前端·javascript·vue.js
Mr.Lee08212 小时前
electron-vite使用vue-i18n,ts 检查报错上不存在属性“$t”
前端·javascript·vue.js·typescript·electron
你的Maya2 小时前
使用 Vite 打包工具库并使用 GitHub Actions 自动化发布npm流程
前端·npm·github
zzzzzzzziu2 小时前
vue3基础
前端·javascript·vue.js
Jasonakeke2 小时前
【JavaWeb】二、HTML 入门
前端·html
2301_796143793 小时前
Vue的指令v-model的原理
前端·javascript·vue.js