Kotlin 39. Dependency Injection依赖注入以及Hilt在Kotlin中的使用,系列2:手动依赖注入

news2024/9/23 2:22:11

一起来学Kotlin:概念:26. Dependency Injection依赖注入以及Hilt在Kotlin中的使用,系列2:手动依赖注入

此系列博客中,我们将主要介绍:

  • Dependency Injection(依赖注入) 概念介绍。网上看了许多关于 DI 的介绍,云里雾里。这里,我们通过通俗易懂地方式对其进行介绍。
  • 手动依赖注入介绍。为了让大家更容易理解 Hilt,我们先介绍如何通过手动的方式实现依赖注入效果。
  • Hilt 注释(annotations)介绍及使用案例
  • MVVM 案例中如何使用 Hilt

此博客主要介绍手动依赖注入。


文章目录

  • 一起来学Kotlin:概念:26. Dependency Injection依赖注入以及Hilt在Kotlin中的使用,系列2:手动依赖注入
    • 1 回顾
    • 2 Hilt 的定义
    • 3 手动依赖注入:提供容器来管理各种依赖
    • 4 手动依赖注入:手动管理生命周期
    • 5 总结


1 回顾

在系列的第一篇博客中,我们介绍了依赖注入的概念,以及为什么需要依赖注入。

简单来说,在场景诸如对象A需要(依赖于)另外一个对象B的实例,将创建对象的任务转移给其他人并直接使用依赖称为依赖注入(DI)。我们在上一篇博客也以实例化汽车 Car 类为例子,解释如何防止这个汽车对象和其依赖的各种对象,例如车轮、引擎等耦合。

比如,下面这个例子,Car 这个实例被创建的时候,会新创建一个 Wheel 的实例,这就是耦合。DI 的目的也就是防止这种情况的发生。

class Car {<!-- -->
    private val wheelA = Wheel()

    fun start() {<!-- -->
        engine.start()
    }
}

fun main(args: Array) {<!-- -->
    val car = Car()
    car.start()
}

此外,我们也罗列了依赖注入的三种实现方式:

  • constructor injection(类的构造函数注入):依赖项是通过类构造函数提供的。
  • setter injection(类字段注入):客户端公开一个 setter 方法,注入器使用它来注入依赖项。
  • interface injection:依赖项提供了一个注入器方法,可以将依赖项注入传递给它的任何客户端。 客户端必须实现一个接口,该接口公开一个接受依赖项的 setter 方法。

2 Hilt 的定义

Hilt provides a standard way to use DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically.

从 Hilt 的定义中,我们可以总结,Hilt主要完成的两件事:

  • 提供了“containers”(容器)用来装各种依赖;
  • 自动管理这些“containers”(容器)的“lifecycles”(生命周期)。

在下面的章节中,我们会通过手动依赖注入,来解释上面的这两件事的含义,以及意义。

3 手动依赖注入:提供容器来管理各种依赖

和上一篇博客一样,我们也通过一些例子来解释手动依赖注入的概念。

下图是 MVVM 的一个基本架构:

请添加图片描述

上图中的箭头是单方向的,意思就是箭头的一端依赖于另外一端。比如,Activity/Fragment 依赖于 ViewModel,而 ViewModel 依赖于 Repository。在安卓的 MVVM架构里,依赖注入的意思就是把 ViewModel 的实例(instance)注入到 Activity/Fragment 类中,同样的道理,Repository 的实例注入到 ViewModel 类中。以此类推,ModelRemoteDataSource 的实例也需要注入到 Repository 类中。

实际上,我们通常做的,就是在 Activity/Fragment 里面直接 new 一个 ViewModel。看起来很方便,但实际上,这是不是和上面的耦合例子非常类似?如果我们只有一个 Activity/Fragment 和一个依赖的 ViewModel,那没什么问题,但如果关系复杂了,依赖注入的优势就很明显了。

比如说,现在需要实现一个用户登录的功能时,用MVVM架构应该是下面这样的:

请添加图片描述

这里,LoginActivity 依赖于 LoginViewModelLoginViewModel 依赖于 UserRepositoryUserRepository 依赖于 UserLocalDataSource 以及 UserRemoteDataSourceUserRemoteDataSource 依赖于 Retrofit

在没有使用依赖注入思想的时候,LoginActivity 大致上应该是这样的:

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // 实例化LoginViewModel
        loginViewModel = LoginViewModel(userRepository)
    }
}

为了满足 LoginViewModel 的实例化,我们需要传入一个 UserRepository 的实例参数 userRepository。这还没有结束,因为 UserRepository 又依赖于 UserLocalDataSource 以及 UserRemoteDataSource。一环扣一环。所以,我们如果把 LoginActivity 写完整,大致是这样的:

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)

        /******新增代码 Begin ******/
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        val userRepository = UserRepository(localDataSource, remoteDataSource)
        /******新增代码 End ******/
       
        loginViewModel = LoginViewModel(userRepository)
    }
}

上面代码中 “新增代码” 段包含了各个依赖之间一环扣一环的关系。所以,在 LoginActivity 类中,我们创建了所有相关的依赖的实例。但是跟我们最初设想的在 LoginActivity 类中只要有 loginViewModel 对象就行,其他的不需要出现在 LoginActivity 类中。我们需要的类似是如下干净的代码:

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        loginViewModel = XXXX
    }
}

我们可以做的,就是新建一个 AppContainer 类,把之前新增的代码都扔到里面去:

class AppContainer {<!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

这样新建的 AppContainer 类还不可以在 LoginActivity 类中使用,因为 AppContainer 类中的这些“依赖”,需要在整个应用(application )全局中使用,所以需要把 AppContainer 类的实例放到 Application() 的子类中:

我们新建一个 MyApplication 类,继承自 Application()

class MyApplication : Application() {<!-- -->
    val appContainer = AppContainer()
}

这样一来,在 LoginActivity 中就可以这么用了:

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)        
    }
}

到此为止,在 LoginActivity 类中已经可以看出依赖注入的编码风格了。

我们在这里实现的 loginViewModel 对象,使用的就是构造函数注入(Constructor Injection)的依赖注入方式:loginViewModel = LoginViewModel(appContainer.userRepository)

更进一步的,假如在我们的安卓应用程序中,除了 LoginActivity 类,其他类也需要 LoginViewModel 的实例对象,那么我们就不能在 LoginActivity 类中新建 LoginViewModel 的实例对象了。还是老办法,作为其中的一个“依赖”,我们要把实现 LoginViewModel 的实例对象放到 “containers”(容器)中。在此,需要用到工厂模式的设计模式,新建一个 Factory 接口,然后在 LoginViewModelFactory 类中实现这个接口,并返回一个 LoginViewModel 的实例:

interface Factory<T> {<!-- -->
    fun create(): T
}

class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {<!-- -->
    override fun create(): LoginViewModel {<!-- -->
        return LoginViewModel(userRepository)
    }
}

接下来的事情就简单了,我们把这个新建的 LoginViewModelFactory 工厂类,放到 AppContainer 中:

class AppContainer {<!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // 新建一个 loginViewModelFactory 对象,在整个application范围内都可以使用
    val loginViewModelFactory = LoginViewModelFactory(userRepository)  
}

然后在 LoginActivity 类中就可以直接使用了(而不是像之前那样,新建一个 LoginViewModel 的实例对象):

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:  
        loginViewModel = appContainer.loginViewModelFactory.create()    
    }
}

4 手动依赖注入:手动管理生命周期

现在很多安卓应用都支持多用户,所以我们需要扩充上面的应用功能:记录不同的用户登录信息。这就需要在 LoginActivity 中增加新的功能,达到下面的目的:

  • 在这个用户登录期间保持对 LoginUserData 类实例对象的访问,退出登录后释放资源;
  • 当新用户登录后,重新新建一个 LoginUserData 类实例对象。

这时我们需要添加一个 LoginContainer 类,用来存储 LoginUserData 类的实例对象和 LoginViewModelFactory 类的实例对象。(TIPS: container 中放的是依赖,也就是各种类的实例对象)

class LoginContainer(val userRepository: UserRepository) {<!-- -->

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

然后把 LoginContainer 应用到之前的 AppContainer 中:

class AppContainer {<!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
   
    //val loginViewModelFactory = LoginViewModelFactory(userRepository)
    // loginViewModelFactory 的实现已经放到LoginContainer中,此处不再需要
   
    // 新建一个loginContainer变量,类型是LoginContainer,初始值是null
    // 因为当用户退出时,其值时null
    var loginContainer: LoginContainer? = null 
}

接下来回到 LoginActivity 类中,我们需要在 LoginActivity 类的 onCreate() 阶段(用户登录),通过 loginContainer 拿到 LoginUserData 的实例对象,而在 onDestroy() 阶段(用户退出)释放相应的资源:

class LoginActivity: Activity() {<!-- -->

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:  
        //loginViewModel = appContainer.loginViewModelFactory.create() 替换成下面代码:  

        // 用户登录,实例化LoginContainer,得到appContainer中的loginContainer 对象
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        // loginViewModel 对象的获取比原来多了一层 loginContainer 对象
        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
       
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {<!-- -->
        // 用户退出,释放资源
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

5 总结

这里,我们总结一下手动依赖注入都干了些啥:

  • 首先新建了一个 AppContainer 类,把 LoginViewModel 需要的各种依赖一股脑都放进去;
  • 为了在应用的其他地方(除了 LoginActivity 以外)使用 LoginViewModel 的实例,我们用工厂类的设计模式在 AppContainer 类容器中实现了一个 loginViewModelFactory对象;
  • 最后为了实现不同用户的登录和登出,我们又新建了一个 LoginContainer 类容器,并放到 AppContainer 类容器中,然后在 LoginActivity 中可以在 onCreate() 中拿到用户登录的信息 loginData,并且在用户登出以后,即在 LoginActivityonDestroy() 中释放资源。

可以预见的,当我们的应用功能越来越复杂的时候(现在只是其中的一个登陆功能),手动依赖注入将会变得不可维护。这就是 Hilt 被使用的原因。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

当IDEA加载一个MAVEN新项目时,加载不成功,无法加载依赖的解决方法

当IDEA加载一个MAVEN新项目时&#xff0c;加载不成功&#xff0c;无法加载依赖的解决方法 此文为练习项目时的错误记录 当使用IDEA引入一个MAVEN新项目时&#xff0c;点击Reload maven按钮加载不成功&#xff0c;显示如下图界面&#xff1a; 在项目中的代码显示报红&#xff0c…

urllib之ProxyHandler代理以及CookieJar的cookie内存传递和本地保存与读取的使用详解

处理更高级操作时(Cookies处理&#xff0c;代理设置)&#xff0c;需要一个强大的工具Handler&#xff0c;可以理解成各种处理器&#xff0c;有处理登录认证的、有处理Cookies的、有处理代理设置的。利用这些几乎可以做到HTTP请求中所有事情。当中urllib.request模块里的 BaseHa…

机器学习:基于朴素贝叶斯对花瓣花萼的宽度和长度分类预测

机器学习&#xff1a;基于朴素贝叶斯对花瓣花萼的宽度和长度分类预测 作者&#xff1a;AOAIYI 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;AOAIYI首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞…

MySQL实战解析底层---基础架构:一条SQL查询语句是如何执行的?

目录 前言 连接器 查询缓存 分析器 优化器 执行器 前言 平时使用数据库&#xff0c;看到的通常都是一个整体比如&#xff0c;有个最简单的表&#xff0c;表里只有一个 ID 字段&#xff0c;在执行下面这个查询语句时&#xff1a; 看到的只是输入一条语句&#xff0c;返回…

微电影广告的内容突破方案

微电影作为新媒体时代背景的产物&#xff0c;深受大众的欢迎&#xff0c;同时&#xff0c;微电影广告在微电影模式环境下应运而生&#xff0c;以自己独特的传播优势&#xff0c;俘获了大量企业主的青睐&#xff0c;也获得了广大青年群体的喜爱。微电影广告欲确保可持续发展&…

Android App异常崩溃处理详解

异常崩溃是Android项目中一个棘手的问题&#xff0c;即使你做了很多的try - catch处理&#xff0c;也不能保证不崩溃&#xff0c;一旦崩溃就会出现下图的弹窗&#xff0c;xx应用就会停止运行这种体验对于用户来说是很差的&#xff0c;所以很明显我们做的app已经崩溃了。 像现在…

社科院与杜兰大学金融管理硕士项目——在这里共同改变,一起前行

人这一生&#xff0c;要走很长的路&#xff0c;才能确定前行的方向&#xff0c;在路上遇到很多的人&#xff0c;才知道与谁同行。在人生旅程中&#xff0c;保持怎样的姿态前行&#xff0c;往往与身边的人有很大关系。身边的人都很努力&#xff0c;你也会跟着努力上进。怀揣着在…

Synchronized,我要一层一层剥开你的心

三种应用方式 修饰实例方法&#xff0c;作用于当前实例加锁&#xff0c;进入同步代码前要获得当前实例的锁。修饰静态方法&#xff0c;作用于当前类对象加锁&#xff0c;进入同步代码前要获得当前类对象的锁。修饰代码块&#xff0c;指定加锁对象&#xff0c;对给定对象加锁&a…

TypeScript 使用 ES6 解构骚操作

TypeScript 使用 ES6 解构骚操作 文章目录TypeScript 使用 ES6 解构骚操作一、TypeScript 对象解构二、TypeScript 函数参数解构四、参考资料&#x1f498;五、推荐博文&#x1f357;一、TypeScript 对象解构 我们都知道 ES6 的数据解构功能很强大&#xff0c;一行命令就能够声…

HTMLCSS常见问题解决

文章目录一、解决img图片底部空白问题1、出现问题的效果2、原理3、解决方式3.1、将图片变为盒子3.2、处理基线3.3、把上级元素的字体大小改成0像素二、解决给子元素设置margin-top父子盒子都向下移动问题1、出现问题的效果2、原理3、解决方式3.1、给父元素设置边框3.2、给父元素…

程序员多赚20k的接私活必备网站

为什么都是程序员&#xff0c;就有人能多赚20k&#xff1f;那是因为副业搞得那么溜啊&#xff01; 今天分享一些程序员搞钱必备的接私活网站&#xff0c;让更多程序员们在工作之余能有另外一份收入。 1.程序员客栈&#xff1a;http://proginn.com 专为程序员服务的软件外包对…

跨境电商代购系统演示说明

首先来看什么是淘宝代购淘宝代购是近年兴起的一种购物模式&#xff0c;是帮国外客户购买中国商品。主要是通过万邦 科技的外贸代购系统&#xff0c;把淘宝、天猫等电商平台的全站商品通过API 接入到你的网站 上&#xff0c;瞬间就可以架设一个有数亿产品的大型网上商城&#xf…

FATE联邦学习centos成功部署

官方文档&#xff1a;https://fate.readthedocs.io/en/latest/deploy/standalone-deploy/#1-description。 我用的文档中的Standalone的第二种安装方式&#xff0c;没用docker。 安装过程 文档上写着确定版本 export version1.7.0但是你别真的用1.7.0啊&#xff01; &#…

ATS认证教学

我用的版本是ATS7.11、系统版本是用最新的ios13.2.1 定义 ATS旨在分析通过UART、USB和蓝牙传输传输的iAP流量、通过USB和无线&#xff08;蓝牙和Wi-Fi&#xff09;传输的CarPlay流量、通过Wi-Fi传输的AirPlay 2流量以及闪电音频流量。 ATS是Apple’s Accessory Test System的…

Cookie、Session、JWT 那些事

文章目录前言一、概念1、Cookie&#xff1a;2、Session&#xff1a;3、JWT二、应用1. 基本使用2. 实现 “退出” 功能总结前言 目前 C/S 模式盛行&#xff0c;HTTP 是其中最常见的通信协议&#xff0c;我们知道 HTTP 协议是无状态的&#xff0c;但是这场景完全不够用。 比如&…

让物流园区可视可控,顺丰供应链与亚马逊云科技的供应链新解法

导读&#xff1a;物流园区如何破解供应链断点&#xff1f;在物流园区附近&#xff0c;我们经常看到周边道路停满了集装箱卡车。这是物流园区的一个典型痛点&#xff0c;由于园区内部业务情况的不可见性&#xff0c;司机们往往到了园区才被告知业务繁忙&#xff0c;需要长时间排…

字符串反转-课后程序(JAVA基础案例教程-黑马程序员编著-第九章-课后作业)

【案例9-2】 字符串反转 【案例介绍】 1.案例描述 在使用软件或浏览网页时&#xff0c;总会查询一些数据&#xff0c;查询数据的过程其实就是客户端与服务器交互的过程。用户&#xff08;客户端&#xff09;将查询信息发送给服务器&#xff0c;服务器接收到查询消息后进行处…

中国版ChatGPT高潮即将到来,解密ChatGPT底层网络架构

2022年11月30日人工智能研究实验室OpenAI发布全新聊天机器人ChatGPT&#xff0c;在中国用户无法访问的前提下&#xff0c;上线仅两个月月活用户就突破了1亿。ChatGPT如同重磅炸弹&#xff0c;一时间火遍全球。面对这一万亿级市场机遇&#xff0c;在国内&#xff0c;无论是资本方…

2.28性能测试概念

一.自动化测试的亮点1)使用注解:避免生成过多对象,造成资源和时间的浪费2)通过static修饰静态变量,全局只创建了一次驱动对象,在测试前再卸载驱动.避免重复创建驱动对象造成时间和资源的浪费3)通过使用参数化,保持用例的简洁,提高了代码的可读性.4)使用测试套件:一次性执行所有…

2022年AI顶级论文 —生成模型之年(下)

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 过去十年来&#xff0c;人工智能技术在持续提高和飞速发展&#xff0c;并不断冲击着人类的认知。 2012年&#xff0c;在ImageNet图像识别挑战赛中&#xff0c;一种神经网络模型&#xff08;AlexNet&am…