11.Hilt简介

依赖注入

Dagger

说到依赖注入,做Android的人都会想到一个库:Dagger。

说到Dagger,大家想到的都是牛逼、高端,又难学又难用。很多使用者用着用着就会掉进了自己亲手用Dagger搭建的迷宫中,越陷越深。

Hilt

所以Android团队在Jetpack中增加了Hilt,它是一个专门针对Android平台的依赖注入库。它并不是提供了依赖注入的能力,而是提供了一种依赖注入的简单实现方式。Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持。

Hilt做的优化包括:

  1. 无需编写大量的Component代码
  2. Scope也会与Component自动绑定
  3. 预定义绑定,例如Application和Activity
  4. 预定义的限定符,例如@ApplicationContext和@ActivityContext

Koin

Koin - a smart Kotlin injection library to keep you focused on your app, not on your tools

Koin是为Kotlin开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。

Hilt、Dagger、Koin等等都是依赖注入库,Google也在努力不断的完善依赖注入库从Dagger到Dagger2在到现在的Hilt,因为依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:

  • 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
  • 在配置scopes范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
  • 代码变得更具可读性。
  • 易于构建对象。
  • 编写低耦合代码,更容易测试。

添加依赖项

首先,将 hilt-android-gradle-plugin 插件添加到项目的根级 build.gradle 文件中:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

然后,应用 Gradle 插件并在 app/build.gradle 文件中添加以下依赖项:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

注意:同时使用Hilt和数据绑定的项目需要Android Studio 4.0或更高版本。

Hilt使用 Java 8 功能。如需在项目中启用Java 8,请将以下代码添加到app/build.gradle文件中:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Hilt常用注解的含义

Hilt常用注解包含@HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides、@EntryPoint等等。

@HiltAndroidApp

  1. 所有使用Hilt的App必须包含一个使用@HiltAndroidApp注解的Application。它会替代Dagger中的AppComponent。
  2. @HiltAndroidApp注解将会触发Hilt代码的生成,作为应用程序依赖项容器的基类。
  3. 生成的Hilt组件依附于Application的生命周期,它也是App的父组件,提供其他组件访问的依赖。
  4. 在Application中设置好@HiltAndroidApp之后,就可以使用Hilt提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver 等等。

@AndroidEntryPoint

Hilt提供的@AndroidEntryPoint注解用于提供Android类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)。

  • Activity:仅仅支持ComponentActivity的子类例如FragmentActivity、AppCompatActivity等等。
  • Fragment:仅仅支持继承androidx.Fragment的Fragment
  • View
  • Service
  • BroadcastReceiver

如果您使用@AndroidEntryPoint为某个Android类添加注释,则还必须为依赖于该类的Android类添加注释。例如,如果您为某个Fragment添加注释,则还必须为使用该Fragment的所有Activity添加注释。

@Inject

Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,它常用于构造函数、非私有字段、方法中。

注意:在构建时,Hilt为Android类生成Dagger组件。然后Dagger遍历您的代码并执行以下步骤:

  • 构建并验证依赖关系,确保没有未满足的依赖关系。
  • 生成它在运行时用于创建实际对象及其依赖项的类。
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

注意:由Hilt注入的字段不能为私有字段。尝试使用Hilt注入私有字段会导致编译错误。

@Module

有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用Hilt模块向Hilt提供绑定信息。

Hilt模块是一个带有@Module注释的类。与Dagger 模块一样,它会告知Hilt如何提供某些类型的实例。与Dagger 模块不同的是,您必须使用@InstallIn为Hilt模块添加注释,以告知Hilt每个模块将用在或安装在哪个Android类中。

常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等),使用@Module注解的类,需要使用@InstallIn注解指定module的范围。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {
}

@InstallIn

使用@Module注入的类,需要使用@InstallIn注解指定module的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的module会绑定到activity的生命周期上。

Hilt提供了以下组件来绑定依赖与对应的Android类的活动范围。

Hilt 提供的组件 对应的 Android 类的活动范围
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View annotated with @WithFragmentBindings
ServiceComponent Service

注意:Hilt没有为broadcast receivers提供组件,因为Hilt直接从ApplicationComponent注入broadcast receivers。

Hilt会根据相应的Android类生命周期自动创建和销毁生成的组件类的实例,它们的对应关系如下表格所示。

Hilt 提供的组件 创建对应的生命周期 销毁对应的生命周期
ApplicationComponent Application#onCreate() Application#onDestroy()
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() View destroyed
ViewWithFragmentComponent View#super() View destroyed
ServiceComponent Service#onCreate() Service#onDestroy()

@Provides

它常用于被@Module注解标记类的内部的方法,并提供依赖项对象。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {

    /**
     * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
     * @Singleton 提供单例
     */
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .build()
    }
}

@EntryPoint

Hilt支持最常见的Android类Application、Activity、Fragment、View、Service、BroadcastReceiver等等,但是您可能需要在Hilt不支持的类中执行依赖注入,在这种情况下可以使用@EntryPoint注解进行创建,Hilt会提供相应的依赖。

如何使用 Hilt 进行依赖注入

我们先来看一个简单的例子,注入HiltSimple并在Application中调用它的doSomething方法。

class HiltSimple @Inject constructor() {
    fun doSomething() {
        Log.e(TAG, "----doSomething----")
    }
}

@HiltAndroidApp
class HiltApplication : Application() {
    @Inject
    lateinit var mHiltSimple: HiltSimple

    override fun onCreate() {
        super.onCreate()
        mHiltSimple.doSomething()
    }
}

Hilt需要知道如何从相应的组件中提供必要依赖的实例。使用@Inject注解来告诉Hilt如何提供该类的实例,@Inject常用于构造函数、非私有字段、方法中。

Hilt vs Koin

我们总共从以下几个方面对Hilt和Koin进行全方面的分析:

  • AndroidStudio支持Hilt在关联代码间进行导航,支持在@Inject修饰的构造器、@Binds或者@Provides修饰的方法、限定符之间进行跳转。

  • 项目结构:完成Hilt的依赖注入需要的文件往往多于Koin。

  • 代码行数:使用 Statistic 工具来进行代码统计,反复对比了项目编译前和编译后,Hilt生成的代码多于Koin,随着项目越来越复杂,生成的代码量会越来越多。

    | 代码行数 | Hilt | Koin | | -------- | ------ | ------ | | 编译之前 | 2414 | 2414 | | 编译之后 | 149608 | 138405 |

  • 编译时间:Hilt编译时间总是大于Koin,这个结果告诉我们,如果是在一个非常大型的项目,这个代价是非常昂贵。

      Hilt:
      BUILD SUCCESSFUL in 35s
      27 actionable tasks: 27 executed
    
      Koin:
      BUILD SUCCESSFUL in 18s
      27 actionable tasks: 27 executed
    
  • 使用上对比:Hilt使用起来要比Koin麻烦很多,其入门门槛高于Koin,在阅读Hilt文档的时候花了好几天时间才消化,而Koin只需要花很短的时间,依赖注入部分的代码Hilt多于Koin,在一个更大更复杂的项目中所需要的代码也更多,也越来越复杂。

    | 依赖注入框架 | Hilt | Koin | | ------------ | ---- | ---- | | 代码行数 | 122 | 42 |

为什么Hilt编译时间总是大于Koin?

因为在Koin中不需要使用注解,也不需要kapt,这意味着没有额外的代码生成,所有的代码都是Kotlin原始代码,所以说 Hilt编译时间总是大于Koin,从这个角度上同时也解释了,为什么会说Koin仅使用功能解析,无额外代码生成。

为什么Koin不需要用到反射?

因为Koin基于kotlin基础上进行开发的,使用了kotlin强大的语法糖(例如 Inline、Reified 等等)和函数式编程,来看一个简单的例子。

inline fun <reified T : ViewModel> Module.viewModel(
    qualifier: Qualifier? = null,
    override: Boolean = false,
    noinline definition: Definition<T>
): BeanDefinition<T> {
    val beanDefinition = factory(qualifier, override, definition)
    beanDefinition.setIsViewModel()
    return beanDefinition
}

内联函数支持具体化的类型参数,使用reified修饰符来限定类型参数,可以在函数内部访问它,由于函数是内联的,所以不需要反射。

Image

If you need compile-time generating library – use Hilt over Dagger, due to simplifications provided. Also consider the best documentation and plenty of examples from the box and in the internet.

In case of run-time generation – use Koin over Kodein because it is simpler for using, has better support with docs and examples.

参考


results matching ""

    No results matching ""