Android开发为什么要用Hilt?new个对象这么简单的事为什么要把它复杂化?看完就知道!

3,439 阅读8分钟

为什么要使用Hilt依赖注入

之前有写过一些文章 Hilt与Koin的对比,和 Android开发中Hilt的常用场景

基本是覆盖了全开发场景,看起来使用起来也是非常简单与实用了,但是同时会引起一些同学的疑问?

为什么要使用依赖注入?直接new对象不香吗?为什么要把简单的问题复杂化?

只是为了让别人看不懂你的代码?让代码看起来几层楼那么高?你是不是在炫技,是不是像装13?

这还真不是,如果说我使用的 Dagger2,还真是有点炫技的意思因为不是那么实用。并且 Dagger 的坑也是真的多,能在大项目中把 Dagger 用好的国内很少见,一般都是国外团队在用。但是谷歌现在推出 Hilt 了之后其实国外团队也都转向 Hilt了 ,因为使用起来真的简单,都是一些场景化的东西,一些固定用法。国内团队很越来越多的接入 Hilt 的依赖注入了。

回归正题,为什么要使用依赖注入?哪种情况下推荐使用依赖注入?就算要用依赖注入,为什么依赖注入推荐使用Hilt?

一、自动管理(灵活与解耦)

首先不是说大家写项目就一定要使用依赖注入,如果大家的项目不是大项目,总共就5、6个,10多个页面,你没必要上依赖注入框架,如果是大项目,分模块,分组件,多人协同开发的,并且可能依赖的对象很复杂,或者说套娃似的对象依赖,那么使用Hilt就非常方便。不同模块/组件的开发人员直接在他自己的组件/模块下定义好对象的提供方式,另一边直接用即可,无需关系依赖的复杂度,和实现的逻辑。并且支持组件化开发(这里Q一下Koin,😅)

我们先看看一些复杂的嵌套依赖,比如我们来一个三层套娃的依赖:

@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

    fun testUser() {
        YYLogUtils.w(userDao.printUser())
        toast(userDao.printUser())
    }

    fun getDaoContent():String{
        return userDao.printUser()
    }

}
@Singleton
class UserDao @Inject constructor(private val user: UserBean) {

    fun printUser(): String {
        return user.toString()
    }

}
data class UserBean(
    val name: String,
    val age: Int,
    val gender: Int,
    val languages: List<String>
)

其他三个类都是必须了,其实也就多了一个这个类,提供UserBean对象

@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

    @Singleton
    @Provides
    fun provideUser(): UserBean {
        return UserBean("newki", 18, 1, listOf("中文", "英文"))
    }

}

使用:

@AndroidEntryPoint
class Demo10DIActivity : BaseVMActivity() {

    @Inject
    lateinit var userServer: UserServer

    override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

    override fun init() {
        findViewById<Button>(R.id.btn_01).click {
            YYLogUtils.w(userServer.toString())
            userServer.testUser()
        }
    }

如果不使用Hilt,自己new对象也能实现

class Demo10DIActivity : BaseVMActivity() {

    lateinit var userServer: UserServer

    override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

    override fun init() {
        //自己new三个对象
         userServer = UserServer(UserDao(UserBean("newki", 18, 1, listOf("中文", "英文"))))

        findViewById<Button>(R.id.btn_01).click {
            YYLogUtils.w(userServer.toString())
            userServer.testUser()
        }
    }

这样new出来的对象,且不说生命周期是跟随页面的,无法保持单例,我们就说如果需求变了,UseDao中需要UserBean和UserProfile2个对象了,如果你是new对象,那么就要到处修改,如果是Hilt的方式,我们就只需要修改UserDao对象的构造即可

@Singleton
class UserDao @Inject constructor(private val user: UserBean,private val profile:UserProfile) {

    fun printUser(): String {
        return user.toString()
    }

}

以上只是举个简单例子,构建一个对象,还要构建一堆其他的对象,并且其他对象的构建同样复杂,并且必须按顺序构建,而且需要的对象的生命周期都不一样,有些生命周期可能和Activity一样,有些可能是单例,所以在构建的时候还要考虑对象声明周期,考虑对象的来源。

特别是在大型项目中很痛苦,因为项目不是一个人写的,大家协同合作开发,看别人的代码也和看天书一样,并不知道同事的对象是如何创建的,如果一个对象的构建方式发生改变,会影响整个的构建过程以及所关联的代码,牵一发而动全身。

这个时候依赖注入框架就派上用场了,我们只用专注于怎么实现功能,对象的依赖关系和生命周期,都让它来帮我们管理,一个Inject,它会按照依赖关系帮我们注入我们需要的对象,并且它会管理好每个对象的生命周期,在生命周期还没结束的情况下是不会重复new的。

所以Hilt依赖注入非常适合大项目,小项目开发者因为项目复杂度低,没遇到这些问题,所以不会理解为什么要用Hilt依赖注入,发出疑问,为什么要让简单的new对象搞这么复杂。

二、生命周期控制

这里说对象的生命周期,其实就是在一定作用域的生命周期,如果只是说单例有点太浅薄,可以说是是在一定范围内的单例。

我们直接new对象是无法控制生命周期的,除非我们使用全局单例的对象,而通过Hilt依赖注入我们可以很方便的实现对象的生命周期的控制。

比如我们普通对象的快速注入方式,直接注解Singleton就标注的是全局范围单例

@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

    fun testUser() {
        YYLogUtils.w(userDao.printUser())
        toast(userDao.printUser())
    }

    fun getDaoContent():String{
        return userDao.printUser()
    }

}

另一种用法是我们使用Module的方式定义依赖注入,那么使用SingletonComponent + Singleton 同样是全局范围单例的意思

@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

    @Singleton
    @Provides
    fun provideUser(): UserBean {
        return UserBean("newki", 18, 1, listOf("中文", "英文"))
    }

}

如果我们想Activity内的单例,我们使用ActivityComponent + ActivityScoped 就是Activity范围的单例。

@Module
@InstallIn(ActivityComponent::class)
class Demo10DIModule {

    @ActivityScoped
    @Provides
    fun provideUser(): UserBean {
        return UserBean("newki", 18, 1, listOf("中文", "英文"))
    }

}

以上两种都是比较常用的作用域,其他的我们还能保证Fragment内单例,View内部单例等等,用法都是同样的用法。

所以对依赖对象的生命周期控制也是Hilt很方便的一个特点,使用new对象是无法做到这一点的。

三、对比其他依赖注入

目前Android主流的也就是三种依赖注入 Dagger2 Hilt Koin。

之前比较过Koin,只能在Kotlin语言环境中使用。并且性能并不会好过Hilt。错误提示也不友好。

Dagger2不是不能用,17年18年的时候特别火,但是学习成本很高,每次创建UI个依赖注入类还得mackproject,并且错误的提示也不友好,

其实我17年就已经在使用Dagger2了,然后自己做了Demo与框架封装,后来做项目中并没有使用,一个是坑太多,一个是同事不会用,学习成本太高。也就放弃使用Dagger2了。

而Hilt其实就是Daggert2的Android场景化的实现,内部对Dagger2进行了封装,在Android开发中使用Hilt更加的简便,学习成本很低,错误提示友好。并且还对ViewModel都可以注入了哦。所以说它是专为Android开发而生。

关于注入的对象内存占用是否有优化的这一点,其实并没有文章或者官方的文档指出有内存优化这一点,仅我自己的测试来说,整个页面如果有多个注入对象和直接new对象相比,感觉注入的对象占用内存稍微少一点,不知道是不是测试的波动,我不理解,如有懂的大佬还望指点一下。

总结

总结为什么要使用Hilt。

  1. 偷懒;自动管理,多对象的自动注入,万一有修改不需要到屎山中到处趴。
  2. 单例;让对象拥有生命周期,无需我们自己手动单例创建,然后去手动注销。
  3. 解耦;不需要到处引入我一些不需要的对象,特别是组件化的项目,另一个组件只管注入,在我的组件中我只管引用。

当然还有其他依赖注入的优点了,只是我觉得这是我使用Hilt最吸引我的三个点。

所以说在当前2022年了,依赖注入我推荐Hilt。关键使用简单,在Android的常用场景下我还做了一些 Demo, 总共就那么多固定的一些用法,之前我写过Demo覆盖Android开发大部分的使用场景, 有需要直接拿走即可,可以查看我之前的文章,在此篇文章开头也有链接。

因为我本人就是开发国外应用,和一些国外的程序员有合作开发项目。顺便说一句,这是依赖注入是国外程序员的必备面试技能,感觉相比国内的开发者而言国外的开发者特别喜欢依赖注入。

好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。