kotlin的dagger hilt依赖注入

依赖注入(dependency injection, di)是设计模式的一种,它的实际作用是给对象赋予实例变量

基础认识

class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 初始化变量val person = Person("Miirym")setContent {DiStudyTheme {}}}
}class Person(val name: String)

以上就是一个基本的di过程:我们创建了一个对象person,并传递了名为Miirym的Test实例。

在此情形下,Miirym的名称是依赖,因为类Person依赖于这个名字,在此之后才有了实例化的变量。

那么di的好处体现在什么地方?我们使用错误的方式展开:

class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 初始化变量val person = Person()setContent {DiStudyTheme {}}}
}class Person {// 去除了主构造函数val name = "Miirym"
}

我们移除了传递name的主构造函数,在此情形下,我们无法向构造函数传递参数,例如name。显然,对于每个Person的实例化对象,它们都会包含相同的名称:Miirym。

更复杂的情形是,我们可能有一个依赖于特定repository(存储库)实例的viewmodel,此时这个repository就成为了我们注入这个viewmodel的依赖。

可能我们会有疑问,为什么在一个viewmodel中,我们会需要不同类型的repository?最常见的一个理由就是测试,同时为了保持灵活性,你需要能够传递任何类型的repository。在我给出的例子中,我传递的是简单的name,但是对于viewmodel而言,传入的repository可能会是任何类型的。

简而言之,传入viewmodel的内容由外部决定。对于这个传入的操作,最简单的方式就是构造函数注入(constructor inject),而这正是我们在第一个示例中做的操作。

dagger hilt简介

dagger hilt帮助我们以简单的方式注入了依赖,省去了构造函数注入的操作(如大量的声明传参的依赖),因此我们能更为集中地管理依赖。

更为重要的一点是,我们可以更便捷的控制依赖的寿命。以单例为例,在一个完整的项目中,例如数据库的单例只能存在一个,但是我们不会只有一个单例;同时我们可以确定特定依赖项的范围,比如限定某个依赖只能依附于某个activity,这样当activity被销毁时,该依赖的内存也会被清理,进而用于其他地方。

接下来我们尝试创建依赖于一个repository的viewmodel,对于app级别的build.gradle引入viewmodel和dagger hilt依赖如下。

    // ViewModel Composeimplementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"//Dagger - Hiltimplementation "com.google.dagger:hilt-android:2.40.5"kapt "com.google.dagger:hilt-android-compiler:2.40.5"implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"kapt "androidx.hilt:hilt-compiler:1.0.0"implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'

同时需要引入kapt和dagger hilt插件。

plugins {...id 'kotlin-kapt'id 'dagger.hilt.android.plugin'
}

引入kapt插件是为了让kotlin处理注解,因为在使用dagger hilt生成代码时,会用到注解。dagger hilt是在编译时注入的,因此在app被启动时、也就是在编译时,我们就知道依赖流向何处了。

对于项目级别的build.gradle我们需要添加依赖。

buildscript {...dependencies {...classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.5"}
}

项目结构示例

项目结构如下。

此处我们模拟一个请求网络的api。

interface MyApi {@GET("test")suspend fun doNetworkCall()
}

在domain层(包含业务实体,用于封装、传输数据)创建 MyRepository接口,用于抽象具体功能。

interface MyRepository {suspend fun doNetworkCall()
}

接着我们创建一个MyRepositoryImpl类,用于在data层实现MyRepository接口。

class MyRepositoryImpl : MyRepository {override suspend fun doNetworkCall() {TODO("Not yet implemented")}}

在此处我希望从MyApi中调用api请求,将内容传递到repository中,这就是di开始的地方:我们希望把MyApi的实例化对象,传递到实现了MyRepository接口的MyRepositoryImpl类中。此处我们继续使用构造方法。

class MyRepositoryImpl(private val api: MyApi // 主构造方法传递依赖
) : MyRepository {override suspend fun doNetworkCall() {TODO("Not yet implemented")}}

 但是在dagger hilt中,它是如何知道我们想将api委托给repository的呢?

创建Module提供依赖项

在app中,在涉及到di时,我们使用多种多样的modules(模块)来作为特定类型依赖项的容器。因此当我们要将api交给repository时,我们要做的操作是新建一个模块来实现这一内容。这样做的好处是,存储在模块中的依赖项会和application存活时长相同,这也使得它们成为效率极高的单例。

因此在实际的app中,如果我们使用较多的依赖,我们可以为其创建相应的模块。例如包含某种广播功能的广播模块,或者是用于提供身份验证存储库的身份验证模块。总而言之,最好是每个模块都能拥有其清晰的职能。

根据以上理论,我们创建一个AppModule。

@Module
@InstallIn(SingletonComponent::class)
object AppModule {}

在此处我们添加了属于dagger hilt的注解:Module和InstallIn。前者表明这是一个用于装载依赖项的容器,后者可以设置这个模块的存活时长。在此处我们设置的是SingletonComponent,代表它的存活时长会如同被注入的application,同时还有其他多种多样的component如下。

SingletonComponent和被注入的application的存活时长相同
ActivityComponent和被注入的activity的存活时长相同
ViewModelComponent和被注入的view model的存活时长相同
ActivityRetainedComponent当activity被recreate、如做旋转屏幕的操作时,该模块不会被销毁
ServiceComponent和被注入的service的存活时长相同

基于我们试图注入MyApi接口,dagger hilt需要知晓如何创建一个retrofit的实例化对象并返回,这就是我们在模块中需要做的事情。

@Module
@InstallIn(SingletonComponent::class)
object AppModule {@Provides@Singletonfun provideMyApi() : MyApi {return Retrofit.Builder().baseUrl("https://test.com").build().create(MyApi::class.java)}
}

根据以上代码,dagger hilt知道了如何创建一个MyApi的实例化对象,每当我们向MyApi请求一个对象时(如在MyRepositoryImpl的主构造方法中),dagger hilt会在模块中尝试寻找一个这样的对象。

我们加入了两个注解:前者告诉dagger hilt该方法提供了依赖项,后者表示这个方法是一个单例。

此处的Singleton和类注解中的SingletonComponent的区别在于:

类注解的决定了该模块下的依赖项的寿命,所有模块下的依赖项的存活时长都如同application;

方法注解的被称为作用域(scope),在此处我们声明该方法返回的MyApi对象是单例的,如果没有这个注解,那么有多个repository要调用这个方法时,返回的MyApi对象就是不唯一的了。

注入依赖项到ViewModel

在一般情况下,创建一个viewmodel可能需要使用ViewModelProvider下的factory类,用于更详细的定义一个viewmodel,但是这样较为复杂。使用dagger hilt可以避免这一情况。

首先创建ViewModel如下,传入依赖项repository。

class MyViewModel(private val repository: MyRepository
) : ViewModel() {}

若使用dagger hilt来寻找主构造函数中需要的依赖项,需要改造如下。

@HiltViewModel
class MyViewModel @Inject constructor(private val repository: MyRepository
) : ViewModel() {}
  1. 在viewmodel外部添加注解@HiltViewModel,声明我们需要使用dagger hilt为该viewmodel注入依赖;
  2. 在主构造函数添加注入注解@Inject和构造函数关键字constructor,向dagger hilt表明我们需要在构造函数中注入这些依赖

经过如上处理,dagger hilt便会从module中寻找是否能提供这些依赖项。所以接下来我们需要在module中添加能够提供viewmodel的方法。

首先在module中创建一个不完整的提供MyRepository方法,因为构建MyRepository需要传入一个MyApi的实例,所以出现了报错;同时注意到,我们先前写的provideMyApi方法,正好返回了MyApi实例。

那么我们是否需要在某个地方调用provideMyApi方法,以此方式来传入MyApi实例呢?

@Module
@InstallIn(SingletonComponent::class)
object AppModule {@Provides@Singletonfun provideMyApi(): MyApi {return Retrofit.Builder().baseUrl("https://test.com").build().create(MyApi::class.java)}@Provides@Singletonfun provideMyRepository(api: MyApi): MyRepository {return MyRepositoryImpl(api)}
}

实际上是不需要的,我们只需要简单的声明需要传入MyApi实例,而dagger hilt会在后台查询是否有能够提供该实例的方法,最终会定位到provideMyApi方法。

经过以上步骤,dagger hilt会将它作为参数传递到我们的provideMyRepository方法,并且创建我们需要的repository;同样的,provideMyRepository方法因为提供了MyRepository实例,同时在我们先前创建viewmodel的主构造函数中请求了MyRepository实例、并使用了依赖注入,所以这个方法也会在后台被dagger hilt调用。

将ViewModel注入UI层

在使用dagger hilt创建viewmodel时,简单的直接创建是不会生效的,我们需要做以下两件事情:

  1. 添加注解@AndroidEntryPoint:当我们需要为安卓组件类(如activity、fragment、service等来自Android Framework的东西)注入依赖时,需要添加注解@AndroidEntryPoint
  2. 创建application类:当repository需要获取context的时候,dagger hilt需要从application获取之,因为我们无法直接在module获取这个context。此时我们需要创建application并为其添加注解@HiltAndroidApp
@AndroidEntryPoint
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {DaggerHiltCourseTheme {val viewModel = hiltViewModel<MyViewModel>()}}}
}
@HiltAndroidApp
class MyApp: Application()

 对于后者dagger hilt是否能真正获取到这一context,我们可以修改MyRepositoryImpl这一实现类来打印context。

@Module
@InstallIn(SingletonComponent::class)
object AppModule {...@Provides@Singletonfun provideMyRepository(api: MyApi, app: Application): MyRepository {return MyRepositoryImpl(api, app)}
}class MyRepositoryImpl(private val api: MyApi,private val appContext: Application
) : MyRepository {init {val appName = appContext.getString(R.string.app_name)Log.i("MyRepositoryImpl", "Hello from the repository: this app name is $appName")}...}

可以看到我们正确的打印了AndroidManifest.xml中的正确默认名称,这也代表我们正确获取了app的context。 

dagger hilt对于相同类型依赖项的区分

如果有两个相同类型的依赖项,dagger hilt是怎么区分我们需要哪一个的?

@Module
@InstallIn(SingletonComponent::class)
object AppModule {...@Provides@Singletonfun provideMyRepository(api: MyApi, app: Application, hello1: String): MyRepository {return MyRepositoryImpl(api, app)}@Provides@Singletonfun provideString1() = "Hello 1"@Provides@Singletonfun provideString2() = "Hello 2"
}

对AppModule修改如上,模拟两个获取字符串的方法,并在provideMyRepository中注入依赖。

此时直接运行会报错,提示该字符串被绑定多次。

 对此情况我们可以添加注解@Named。

@Module
@InstallIn(SingletonComponent::class)
object AppModule {...@Provides@Singletonfun provideMyRepository(api: MyApi,app: Application,@Named("hello1") hello1: String): MyRepository {return MyRepositoryImpl(api, app)}@Provides@Singleton@Named("hello1")fun provideString1() = "Hello 1"@Provides@Singleton@Named("hello2")fun provideString2() = "Hello 2"
}

更为直接的方式:绑定抽象类

在AppModule中我们使用了MyRepositoryImpl类来提供MyRepository接口的实现,但实际上如果想要注入一个接口或抽象类,我们有更简便的方法。

首先去除AppModule中提供MyRepository的方法,并新建一个抽象Module用于直接提供MyRepository。这样做的好处在于dagger hilt会生成较少的代码而获得更佳的性能。

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {// 这是提供依赖项的另一种方式,所以实际上不命名为provide,而是bind@Binds@Singletonabstract fun bindMyRepository(myRepositoryImpl: MyRepositoryImpl) : MyRepository
}

其次修改MyRepositoryImpl类,即MyRepository接口的实现类。为其添加注解@Inject和构造方法关键字。

只要dagger hilt知道如何创建构造函数中的这些依赖项,它也会自动地知道如何创建这个抽象类或结构的实现类。

class MyRepositoryImpl @Inject constructor(private val api: MyApi,private val appContext: Application
) : MyRepository {init {val appName = appContext.getString(R.string.app_name)Log.i("MyRepositoryImpl", "Hello from the repository: this app name is $appName")}override suspend fun doNetworkCall() {TODO("Not yet implemented")}}

因此,无论app中任何类型的类,只要它有一个包含了@Inject注解的构造方法,我们都不需要为它创建provide方法(例如我们去除掉的)。

因此,在抽象module中,我们传入抽象的bind方法中的依赖项,只是为了在每次注入一个抽象类时,dagger hilt可以确定是使用哪一个特定的实现类。

Service注入的方式——字段注入

通常情况下的注入是这样的。

@AndroidEntryPoint
class MyService @Inject constructor(private val myRepository: MyRepository
): Service() {override fun onBind(p0: Intent?): IBinder? {TODO("Not yet implemented")}}

在activity中,我们会为它写一个注入构造函数,在其中传入repository;但是对于Service来说,它不能拥有构造函数。那么我们要如何在活动中获取repository?

@AndroidEntryPoint
class MyService: Service() {@Injectlateinit var repository: MyRepositoryoverride fun onCreate() {super.onCreate() // 我们的repository注入实际上就发生在super.onCreate()中repository.doNetworkCall()}override fun onBind(p0: Intent?): IBinder? {TODO("Not yet implemented")}}

答案是使用字段注入。我们使用lateinit关键字定义它、并使用@Inject注解,此后在super.onCreate方法中,dagger hilt就会完成这个变量的依赖注入,无需我们手动初始化。

Lazy Injection

指的是延迟注入依赖项。

修改MyViewModel,使用dagger包下的lazy修饰依赖项。

通常情况下,依赖项会在我们注入时立刻构建,但在使用lazy修饰后,它会在我们第一次使用时创建。

修改后我们去运行代码,会发现 MyRepositoryImpl类的init代码块中的打印不出现了,这是因为我们没有使用它。我们可以简单地在MyViewModel的init代码块获取注入的依赖项repository,这样打印就会重新出现了。

@HiltViewModel
class MyViewModel @Inject constructor(private val repository: Lazy<MyRepository>
) : ViewModel() {init {repository.get() // 获取MyRepositoryImpl的实例化对象,此时被使用了}}

对于惰性注入的常见场景是身份验证,比如用户在输入用户名和密码之后,我们才开始启用repository的获取。

参考文献:The Ultimate Dagger-Hilt Guide (Dependency Injection) - Android Studio Tutorial

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/2046.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Uniapp判断设备是安卓还是 iOS,并调用不同的方法

在 UniApp 中&#xff0c;可以通过 uni.getSystemInfoSync() 方法来获取设备信息&#xff0c;然后根据系统类型判断当前设备是安卓还是 iOS&#xff0c;并调用不同的方法。 示例代码 export default {onLoad() {this.checkPlatform();},methods: {checkPlatform() {// 获取系…

【MySQL】MVCC详解, 图文并茂简单易懂

欢迎来到啊妮莫的学习小屋 祝读本文的朋友都天天开心呀 目录 MVCC简介快照读与当前读快照读当前读 隔离级别隐藏字段和Undo Log版本链✨MVCC原理--ReadView✨ReadView简介设计思路适用隔离级别重要内容 ReadView规则MVCC整体流程 不同隔离级别下的MVCC读已提交可重复读 总结 M…

VSCode Live Server 插件安装和使用

VSCode Live Server是一个由Ritwick Dey开发的Visual Studio Code扩展插件&#xff0c;它提供了一个带有实时重载功能的本地开发服务器。在VSCode中安装和使用Live Server插件进行实时预览和调试Web应用程序。这将大大提高前端开发效率&#xff0c;使网页设计和开发变得更为流畅…

MC1.12.2 macOS高清修复OptiFine运行崩溃

最近在玩RLCraft&#xff0c;在windows中运行正常的&#xff0c;移植到macOS中发现如果加载OptiFine模组就会崩溃 报错日志 报错日志如下&#xff0c;其中已经包含了各种版本信息&#xff0c;我就不单独说明了。这里说一下&#xff0c;报错的时候用的是oracle jdk x64的&…

医学图像分割半监督学习记录

半监督学习中&#xff0c;一部分数据带标签&#xff0c;一部分不带标签&#xff0c;在模型训练过程中&#xff0c;带标签的数据我们注重分类&#xff0c;无标签的数据我们注重分布。 半监督坚持一致性正则&#xff08;consistency regularization&#xff09;来进行半监督学习&…

12 USART串口通讯

1 串口物理层 两个设备的“DB9接口”之间通过串口信号建立连接&#xff0c;串口信号线中使用“RS232标准”传输数据信号。由于RS232电平标准的信号不能直接被控制器直接识别&#xff0c;所以这些信号会经过“电平转换芯片”转换成控制器能识别的“TTL校准”的电平信号&#xff…

工程水印相机结合图纸,真实现场时间地点,如何使用水印相机,超简单方法只教一次!

在工程管理领域&#xff0c;精准记录现场信息至关重要。水印相机拍照功能&#xff0c;为工程人员提供了强大的现场信息记录工具&#xff0c;助力工程管理和统计工程量&#xff0c;更可以将图片分享到电脑、分享给同事&#xff0c;协同工作。 一、打开图纸 打开手机版CAD快速看图…

abap安装cl_json类

文章来自 SAP根据源码导入/ui2/cl_json类 - pikeduo - 博客园 新建一个se38程序&#xff0c;把源码放到里&#xff0c;源码如下 *----------------------------------------------------------------------* * CLASS zcl_json DEFINITION *----------------------------…

day09_kafka高级

文章目录 kafka高级今日课程内容核心概念整理Kafka的数据位移offset**为什么 Kafka 的 offset 就像是“书签”&#xff1f;****实际意义** Kafka的基准/压力测试测试生产的效率测试消费的效率 Kafka的分片与副本机制kafka如何保证数据不丢失生产者端Broker端消费者端相关参数 K…

vue2制作长方形容器,正方形网格散点图,并且等比缩放拖动

需求&#xff1a;有个长方形的容器&#xff0c;但是需要正方形的网格线&#xff0c;网格线是等比缩放的并且可以无线拖动的&#xff0c;并且添加自适应缩放和动态切换&#xff0c;工具是plotly.js,已完成功能如下 1.正方形网格 2.散点分组 3.自定义悬浮框的数据 4.根据窗口大小…

Spring Boot 2 学习指南与资料分享

Spring Boot 2 学习资料 Spring Boot 2 学习资料 Spring Boot 2 学习资料 在当今竞争激烈的 Java 后端开发领域&#xff0c;Spring Boot 2 凭借其卓越的特性&#xff0c;为开发者们开辟了一条高效、便捷的开发之路。如果你渴望深入学习 Spring Boot 2&#xff0c;以下这份精心…

【PyQt】如何在mainwindow中添加菜单栏

[toc]如何在mainwindow中添加菜单栏 如何在mainwindow中添加菜单栏 主要有两种方法&#xff1a; 1.直接创建mainwindow进行添加 2.使用ui文件加载添加 第二种方法更为常见&#xff0c;可以应用到实际 1.直接创建mainwindow进行添加 import sysfrom PyQt5.QtWidgets import …

Vue如何构建项目

目录 1.安装Node.js 2.换源(建议) 3.选择一个目录 4.创建一个vue项目 5.验证是否成功 1.安装Node.js 安装18.3或更⾼版本的 Nodejs 点击下载->Node.Js中文网 node -v npm -v 安装好后在windows的cmd窗口下运行 如果能运行出结果就说明安装好了。 2.换源(建议) //…

uniapp 小程序 textarea 层级穿透,聚焦光标位置错误怎么办?

前言 在开发微信小程序时&#xff0c;使用 textarea 组件可能会遇到一些棘手的问题。最近我在使用 uniapp 开发微信小程序时&#xff0c;就遇到了两个非常令人头疼的问题&#xff1a; 层级穿透&#xff1a;由于 textarea 是原生组件&#xff0c;任何元素都无法遮盖住它。当其…

[c语言日寄]精英怪:三子棋(tic-tac-toe)3命慢通[附免费源码]

哈喽盆友们&#xff0c;今天带来《c语言》游戏中[三子棋boss]速通教程&#xff01;我们的目标是一边编写博文&#xff0c;一边快速用c语言实现三子棋游戏。准备好瓜子&#xff0c;我们计时开始&#xff01; 前期规划 在速通中&#xff0c;我们必须要有清晰的前期规划&#xf…

Chatper 4: Implementing a GPT model from Scratch To Generate Text

文章目录 4 Implementing a GPT model from Scratch To Generate Text4.1 Coding an LLM architecture4.2 Normalizing activations with layer normalization4.3 Implementing a feed forward network with GELU activations4.4 Adding shortcut connections4.5 Connecting at…

【Vim Masterclass 笔记13】第 7 章:Vim 核心操作之——文本对象与宏操作 + S07L28:Vim 文本对象

文章目录 Section 7&#xff1a;Text Objects and MacrosS07L28 Text Objects1 文本对象的含义2 操作文本对象的基本语法3 操作光标所在的整个单词4 删除光标所在的整个句子5 操作光标所在的整个段落6 删除光标所在的中括号内的文本7 删除光标所在的小括号内的文本8 操作尖括号…

Termora跨平台 SSH/SFTP/Terminal 客户端工具

前言 Termora一款强大的终端模拟与SSH客户端工具&#xff0c;集SFTP传输、跨平台兼容、Zmodem协议、SSH端口转发、配置同步、宏录制、关键词高亮、密钥管理、多会话命令发送及数据加密于一体&#xff0c;专为追求高效远程工作的您设计。无论是开发、管理还是日常任务&#xff…

音视频入门基础:RTP专题(1)——RTP官方文档下载

一、引言 实时传输协议&#xff08;Real-time Transport Protocol&#xff0c;简写RTP&#xff09;是一个网络传输协议&#xff0c;由IETF的多媒体传输工作小组1996年在《RFC 1889》中公布的。 RTP作为因特网标准在《RFC 3550》有详细说明。而《RFC 3551》详细描述了使用最小…

JVM之垃圾回收器ZGC概述以及垃圾回收器总结的详细解析

ZGC ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器&#xff0c;基于 Region 内存布局的&#xff0c;不设分代&#xff0c;使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法 在 CMS 和 G1 中都用到了写屏障&#xff0c;而 ZGC 用到了读屏障 染色指针&a…