请选择 进入手机版 | 继续访问电脑版

热点推荐

    查看: 78|回复: 0

    Jetpack - Hilt

    [复制链接]
  • TA的每日心情
    开心
    昨天 00:08
  • 签到天数: 362 天

    [LV.8]以坛为家I

    5万

    主题

    5万

    帖子

    16万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    160408
    发表于 7 天前 | 显示全部楼层 |阅读模式
    Jetpack - Hilt
      h9 f* A3 F5 s* o! Z, z2 {5 h
    9 G2 x* y0 ~: \

      . ^# x3 z" Q% s; {8 O1 o( F6 C
    • 依赖注入、依赖注入框架
      5 a9 c; ^' C9 P4 D  I' I
    • Android 常用的依赖注入框架
      / d/ q' `, l) G) `9 @/ E: F
    • Hilt 的简单使用8 V9 d5 E. b5 |, A* B( u  a% t8 B
    1. 依赖注入、依赖注入框架1 O- ?* {% X) }2 V/ a  f, E7 v

    - t" h6 ?+ f- d) h0 Q' o/ [: c1.1 依赖注入7 f* Z3 l# F# W7 V& `: r

    $ N( i" P0 s+ C7 f* c$ v7 G$ S/ t依赖注入的英文名是 Dependency Injection,简称 DI。其作用一言以蔽之:解耦
    $ I" i; i4 {) V  L- |% ]举个栗子:
    % Q% i0 _$ x3 \; j: P2 b有一家卡车配送公司,只有一辆卡车用来送货。接到一个配送订单,客户委托配送两台电脑。可编写如下代码:. a( D' _# l/ T5 i9 X  |) {* i
    // 定义一个卡车 Truck,卡车有一个 deliver() 函数用于执行配送任务// 在 deliver() 函数中先把两台电脑装上卡车,再进行配送class Truck {    private val computer1 = Computer()    private val computer2 = Computer()    fun driver() {        loadToTruck(computer1)        loadToTruck(computer2)        beginToDeliver()    }}上面在 Truck 类中创建了两台电脑的实例,然后再对它们进行配送。卡车既要会送货,也要会生产电脑,使得卡车和电脑这两样不相干的东西耦合到一起去了,造成耦合度过高。! k. u; V+ F4 w
    若又接到一个新的订单,去配送手机,那这辆卡车还要会生产手机才行。若增加配送蔬果的订单,那么这辆卡车还要会种地。。。最后发现,这已经就不是一辆卡车了,而是一个商品制造中心了:- a9 \' ~: M/ P- @
    其实,卡车并不需要关心配送的货物具体是什么,它的任务只需负责送货。即卡车是依赖于货物的,给了卡车货物,它就去送货,不给卡车货物,它就待命,修改代码如下:
    $ y9 @% r4 C* g- k% x& d// 在Truck类中添加了货物cargos字段,卡车是依赖于货物的class Truck {    lateinit var cargos: List    fun driver() {        for(cargo in cargos) {            loadToTruck(cargo)        }        beginToDeliver()    }}这样,卡车不再关心任何商品制造的事情,而是依赖了什么货物,就去配送什么货物,只做本职应该做的事情。
    1 d2 Y! n: Z6 N5 Q! y这种让外部帮卡车初始化需要配送的货物的写法,就称之为:依赖注入。即让外部帮你初始化你的依赖,就叫依赖注入。
      S3 _9 ]4 O! H. a7 c5 V0 D) Q6 H1.2 依赖注入框架0 [' F! H4 f  T# L' P
    ! |) M  A! P1 t6 c; R
    目前 Truck 类设计得比较合理了,但还存在问题。: R. @2 N! c# f  ?8 r8 s% h
    若此时身份变成了一家电脑公司老板,该如何让一辆卡车来帮忙运送电脑呢?也许会很自然的写出如下代码:* e! L" c+ }4 Q! d% ]
    class ComputerCompany {    private val computer1 = Computer()    private val computer2 = Computer()    fun deliverByTruck() {        val truck = Truck()        truck.cargos = listof(computer1, computer2)        truck.deliver()    }}上面代码同样也存在高耦合度问题:在 deliverByTruck() 函数中,为了让卡车送货,自己制造了一辆卡车。这明显是不合理的,电脑公司应该只负责生产电脑,它不应该去生产卡车。0 i5 D# J* ?9 N
    更加合理的做法是,让卡车配送公司派辆空闲的卡车过来(就不用自己造车了),当卡车到达后,再将电脑装上卡车,然后执行配送任务即可。如下:
    ! ^- p0 S; w$ ^
    使用这种设计结构,就有很好的扩展性。若现在又有一家蔬果公司需要找一辆卡车来送菜,就完全可以使用同样的结构来完成任务,如下:
    - D9 i$ d3 R$ E3 E" H/ e6 R- A
    上图中呼叫卡车公司并让他们安排空闲车辆的这个部分,其实可以通过自己手写来实现,也可借助一些依赖注入框架来简化这个过程。; d; h3 E; _$ ]; f6 x
    因此,依赖注入框架的作用就是为了替换下图所示的部分:3 X: A; \% h% [5 W
    2. Android常用的依赖注入框架' u8 u' k) Q5 K  v

    2 e9 O! }! R" n. \6 R7 `9 w2.1 Dagger  Y, S7 l5 Y7 N* e. D
    $ d, ?) u) h+ g$ ~9 k9 L
    由Square公司开源,基于Java反射去实现的,从而有两个潜在的隐患:
    6 L) H& J# `" ]: \& H, i. v: f
      / {5 Z$ G- O# l. J
    • 反射是比较耗时的,用这种方式会降低程序的运行效率。(这问题不大,现在的程序中到处都在用反射
      & T. q# I6 w5 E0 I( x9 k/ W
    • 依赖注入框架的用法总体来说比较有难度,很难一次性编写正确。而基于反射实现的依赖注入功能,在编译期无法得知依赖注入的用法是否正确,只能在运行时通过程序是否崩溃来判断。这样测试的效率低下,容易将一些 bug 隐藏得很深。
      1 e& M5 L! h; p
    2.2 Dagger2
    $ L0 e/ ?; p0 O9 S7 A: C
    9 c- ]3 C; d" Q8 U# Z, I! e由 Google 开发,基于 Java 注解实现的,把 Dagger1 反射的那些弊端解决了:- j7 D4 o; Y" t' H4 b+ P" Z
    通过注解,Dagger2 会在编译时期自动生成用于依赖注入的代码,不会增加任何运行耗时。另外,Dagger2 会在编译时检查依赖注入用法是否正确,若不正确则会直接编译失败,从而将问题尽可能早地抛出。即项目正常编译通过,说明依赖注入的用法基本没问题了。8 d4 g$ w+ ?2 U; c3 n, b
    但 Dagger2 使用比较复杂,若不能很好地使用它,可能会拖累你的项目,甚至会将一些简单的项目过度设计。; h0 m$ n5 u. J4 f$ K- t
    2.3 Hilt
    % O2 O2 [: O( w3 l1 M* H/ e
    $ f9 z" W/ y; ^$ S4 qGoogle 发布了 Hilt,是在依赖项注入库 Dagger 的基础上构建而成,一个专门面向 Android 的依赖注入框架。
    7 O# C' p- v1 p! B6 x相比于 Dagger2,Hilt 最明显的特征就是: 简单、提供了 Android 专属的 API。  {  [; _+ i: }) B8 P0 K9 V
    3. Hilt 的简单使用
    5 B. e" |; z3 g  w! O% w+ g9 w" |# e' |1 W
    3.1 引入Hilt6 }* F( j1 l* z# {# C4 d' @" t0 F

    ! c- u7 Y6 n/ C1 Q7 f% s' [6 Z第一步,在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:
    + T4 |$ m* }! Z! l: z7 [, gbuildscript {    ...    dependencies {        ...        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.31.1-alpha'    }}接下来,在 app/build.gradle 文件中,引入 Hilt 的插件并添加 Hilt 的依赖库:
    7 L+ _6 e9 t: g/ Oplugins {    ...    id 'kotlin-kapt'    id 'dagger.hilt.android.plugin'}dependencies {    // hilt    implementation "com.google.dagger:hilt-android:2.31.1-alpha"    kapt "com.google.dagger:hilt-android-compiler:2.31.1-alpha"}最后,Hilt 使用 Java 8 功能。如需启用 Java 8,在 app/build.gradle 文件中添加以下代码:
    4 U0 h9 q% b7 Zandroid {    ...    compileOptions {        sourceCompatibility JavaVersion.VERSION_1_8        targetCompatibility JavaVersion.VERSION_1_8    }}这就成功将 Hilt 引入到项目当中了。5 n7 k# U. ]- w2 j. b
    3.2 Hilt 的简单用法1 V3 Z, l4 n6 j* C
    + Z) J1 c5 g3 `, Y! S/ D
    在 Hilt 当中,必须要自定义一个 Application 才行,否则 Hilt 将无法正常工作。如下:6 |, [( A# Z) C/ `8 f- L0 C
    // 注解 @HiltAndroidApp 会触发 Hilt 的代码生成操作,// 生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。@HiltAndroidAppclass MyApplication: Application() {}在 Application 类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint 注解的其他 Android 类提供依赖项。
    4 C2 X( p+ `$ e+ L$ CHilt 目前支持以下 Android 类:
    " a5 x& q4 S+ m' O( g  G
      4 S0 X" }4 G, F3 E6 U! K
    • Application(通过使用 @HiltAndroidApp)
      * I4 T2 \5 i5 D
    • Activity、Fragment、View、Service、BroadcastReceiver(通过使用 @AndroidEntryPoint)
      1 X& i( A, E5 K0 s: b9 H8 _
    以 Activity 为例,在 MainActivity 中进行依赖注入:! |9 x/ ^3 t8 D% x8 k: u1 C4 C* ~1 Q
    @AndroidEntryPointclass MainActivity: AppCompatActivity() {}如把上面的 Truck 类注入到 MainActivity 当中:
    / [% |, K% O- U) [1 Q) K@AndroidEntryPointclass MainActivity: AppCompatActivity() {    // 步骤二:在 truck 字段的上方声明了一个 @Inject 注解    // 即希望通过 Hilt 来注入 truck 这个字段    @Inject    lateinit var truck: Truck    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        truck.driver()    }}// 步骤一:在 Truck 类的构造函数上声明了一个 @Inject 注解// 即告诉 Hilt,可以通过这个构造函数来安排一辆卡车class Truck @Inject constructor() {    fun driver() {        println("卡车运输货物")    }}这样在 MainActivity 中并没有去创建 Truck 的实例,只是用 @Inject 声明一下,就可以调用它的 deliver() 方法,即用 Hilt 完成了依赖注入的功能。
    , O% d# ~+ U6 P* }注:Hilt 注入的字段是不可以声明成 private 的。
    0 R& z9 \% L& F) j0 E3.3 带参数的依赖注入0 Z4 O, t" z, u' f1 I3 p$ A4 Q" O

    ! I# \5 ^+ s  e$ }比如在 Truck 类的构造函数中增加了一个 Driver 参数:0 {& Q, ]7 E# k* m# E, g9 N0 Q3 v2 M% _
    class Truck @Inject constructor(val driver: Driver) {    fun driver() {        println("卡车运输货物,司机是 $driver")    }}class Driver @Inject constructor() {}在 Driver 类的构造函数上声明了一个 @Inject 注解,这样 Driver 类就变成了无参构造函数的依赖注入方式。即 Truck 的构造函数中所依赖的所有其他对象都支持依赖注入了,那么 Truck 才可以被依赖注入。6 |' o+ D$ k6 X2 w
    3.4 接口的依赖注入. ?$ B! F# g0 W+ y. l( T; f

    2 {. E1 D  o) R/ A" b* T* H3 I如定义个 Engine 接口和它的实现类如下:: ]3 w$ f' i/ p! l' i
    interface Engine {    fun start()    fun shutdown()}class GasEngine @Inject constructor() : Engine {    override fun start() {        println("燃油车 start")    }    override fun shutdown() {        println("燃油车 shutdown")    }}接下来需要定义一个抽象类,使用 @Binds 注入接口实例 :
    + R$ {; q6 P  S. ^# W4 G$ P3 W// 在 EngineModule 的上方声明一个 @Module 注解,表示这一个用于提供依赖注入实例的模块// @InstallIn(ActivityComponent::class),表示把这个模块安装到Activity组件当中@Module@InstallIn(ActivityComponent::class)abstract class EngineModule {   // 1. 定义一个抽象函数(因为并不需实现具体的函数体)   // 2. 这个抽象函数的函数名叫什么都无所谓,也不会调用它。   // 3. 抽象函数的返回值必须是Engine,表示用于给Engine类型的接口提供实例。   // 4. 在抽象函数上方加上@Bind注解,这样Hilt才能识别它。   @Binds   abstract fun bindEngine(gasEngine: GasEngine): Engine}定义好抽象类 EngineModule 后,修改 Truck 类的代码如下:" Y9 y8 y+ ~! u* Z5 P3 R
    class Truck @Inject constructor(val driver: Driver) {    @Inject     lateinit var engine: Engine    fun driver() {        engine.start()        println("卡车运输货物,司机是 $driver")        engine.shutdown()    }}这样,Hilt 就向 engine 字段注入了一个 GasEngine 的实例,也就完成了给接口进行依赖注入。+ c/ O3 B* f6 ]; X! S. i- p) @) g
    3.5 给相同类型注入不同的实例. F# ], Z) \3 E* ]8 n
    # J. r4 p+ x( n
    比如再有个 Engine 接口的实现类:
    ( c; A4 T% |0 mclass ElectricEngine @Inject constructor() : Engine {    override fun start() {        println("新能源车 start")    }    override fun shutdown() {        println("新能源车 shutdown")    }}此时,通过 EngineModule 中的 bindEngine() 函数为 Engine 接口提供实例,这个实例要么是 GasEngine,要么是 ElectricEngine,如何同时为一个接口提供两种不同的实例呢?
    & B: j/ V% P9 b1 B8 W5 Y4 M8 {这时就要借助 Qualifier注解 来解决。Qualifier 注解的作用是给相同类型的类或接口注入不同的实例。
    " q/ ^8 ?5 \! }; A, H  m分别定义两个注解,如下:
    * T) g% K6 D2 J" v" U// 注解的上方必须使用 @Qualifier 进行声明。// 注解 @Retention,是用于声明注解的作用范围// 选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但无法通过反射去访问这个注解@Qualifier@Retention(AnnotationRetention.BINARY)annotation class BindGasEngine@Qualifier@Retention(AnnotationRetention.BINARY)annotation class BindElectricEngine定义好上面两个注解后,把它们分别添加到 EngineModule里对应的方法中:
    ( ?5 ^# n' Q/ T& X, K9 k@Module@InstallIn(ActivityComponent::class)abstract class EngineModule {   @BindGasEngine   @Binds   abstract fun bindEngine(gasEngine: GasEngine): Engine   @BindElectricEngine   @Binds   abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine}最后修改 Truck 类中的代码如下:$ `% Y8 Z+ d  _7 w7 O0 i
    class Truck @Inject constructor(val driver: Driver) {    @BindGasEngine    @Inject     lateinit var gasEngine: Engine    @BindElectricEngine    @Inject     lateinit var electricEngine: Engine    fun driver() {        gasEngine.start()        electricEngine.start()        println("卡车运输货物,司机是 $driver")        gasEngine.shutdown()        electricEngine.shutdown()    }}这样就完成了给相同类型注入不同实例。1 l5 f9 U# `& B
    3.6 第三方类的依赖注入$ V- ]3 Y2 |8 }/ X! l

    4 [. b+ r! p- [% [; s" U给第三方类的依赖注入需要使用 @Provides 注解,如给 OkHttpClient、Retrofit 类型提供实例如下:5 |  Y" s% m0 P$ |
    @Module@InstallIn(ActivityComponent::class)class NetModule {    @Provides    fun provideOkHttpClient() : OkHttpClient {        return OkHttpClient.Builder()            .connectTimeout(0, TimeUnit.SECONDS)            .readTimeout(0, TimeUnit.SECONDS)            .writeTimeout(0, TimeUnit.SECONDS)            .build()    }    @Provides    fun provideRetrofit(okHttpClient: OkHttpClient) : Retrofit {         return Retrofit.Builder()            .addConverterFactory(GsonConverterFactory.create())            .baseUrl("http:xxx.com")            .client(okHttpClient)            .build()    }}在 provideOkHttpClient()、provideRetrofit() 函数的上方加上 @Provides 注解,Hilt 就能识别它。9 [8 E1 S; X2 b
    3.7 Hilt 内置组件和组件作用域* ?: `5 W% k# P( y# D8 c

    * ^2 _* z* V; D3 J7 }Hilt 一共内置了7种组件类型,分别用于注入到不同的场景,如 @InstallIn(ActivityComponent::class),就是把这个模块安装到 Activity组件当中,如下表:
      y9 H0 B; j9 m% P: \" K
    Hilt 一共提供了7种组件作用域注解,和上面的7个内置组件分别是一一对应的,如下表:
    3 {: S8 [8 z' D& _) U+ e, D
    * G& F/ f+ m; h' ?: n
    若想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton。5 j- w1 U) Z) @* ]% ]9 i
    若想要在某个 Activity,以及它内部包含的 Fragment 和 View 中共用某个对象的实例,那么就使用@ActivityScoped。( A, H! }; R. O5 }& C
    以此类推。。。
    " e2 S8 e' x/ [8 i作用域的包含关系如下:
    5 \+ o" k( I/ U+ ]( p  x) `& Q$ j" Y' z& @
    即,对某个类声明了某种作用域注解之后,这个注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。/ Q4 D8 v& A$ a; r# r! v% c0 J: l
    如 @Singleton 注解的箭头可以指向所有地方。2 r9 z8 F/ ?' u2 L3 }; h
    如 @ServiceScoped 注解的箭头无处可指,所以只能限定在 Service 自身当中使用。
    ( d/ G! F6 [- b% h* Q* T如 @ActivityScoped 注解的箭头可以指向Fragment、View 当中。; v7 @5 k* L% y9 r. e
    3.8 Hilt 中的预定义限定符
    ( I5 }- T3 l6 H1 Z3 n! v: i+ Z
    + v/ }! m+ z- |! q+ \' JHilt 提供了一些预定义的限定符。
    + b( C/ |: n0 z- Z例如,需要来自应用或 Activity 的 Context 类,就可以用 Hilt 提供的 @ApplicationContext 和 @ActivityContext 限定符。, N; H3 H; E+ z( h: [
    用法很简单,只需要在 Context 参数前加上一个 @ApplicationContext 注解即可:
    6 W# k4 O% ?5 Z* z; v! T- m@Singletonclass Driver @Inject constructor(@ApplicationContext val context: Context) {}// 这边 @ApplicationContext 或 @ActivityContext 可以去掉,Hilt 也能识别class Driver @Inject constructor(val application: Application) {}class Driver @Inject constructor(val activity: Activity) {}若要依赖于自己编写的 MyApplication 的,可以定义个 ApplicationModule 如下:" u1 S" A" Y( M; J* Y1 S/ Z
    @Module@InstallIn(ApplicationComponent::class)class ApplicationModule {    @Provides    fun provideMyApplication(application: Application): MyApplication {        return application as MyApplication    }}使用如下:5 o) R2 n6 Y% R' a3 {) @! e1 a  N/ z
    class Driver @Inject constructor(val application: MyApplication) {}3.9 ViewModel 的依赖注入4 p) S0 i& H7 Q4 M9 r
    ' y; f2 H$ x& K- T3 W4 R
    在 MVVM 架构中,ViewModel 层只是依赖于仓库层,它并不关心仓库的实例是从哪儿来的,因此由 Hilt 去管理仓库层的实例创建再合适不过了。& M2 F. y% E, R! A3 K' O
    常见的 ViewModel 依赖注入过程如下:. n0 A) e: S* p) X7 o* D
    首先有个仓库 Repository 类:$ f. F. T' Q3 Q. a
    // 由于 Repository 要依赖注入到 ViewModel 当中,所以需要加上 @Inject 注解class Repository @Inject constructor() {    ...}然后有一个 MyViewModel 继承自 ViewModel ,用于表示 ViewModel 层:( x* ^/ x' N+ v/ F' {
    // @HiltViewModel 注解,是专门为 ViewModel 提供的// 构造函数中要声明 @Inject 注解,在 Activity 中才能使用依赖注入的方式获得 MyViewModel 的实例@HiltViewModelclass MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {    ...}接下来修改 MainActivity 如下:0 S( d( i* g6 W4 u" e
    @AndroidEntryPointclass MainActivity : AppCompatActivity() {    @Inject    lateinit var viewModel: MyViewModel    ...}这样在 MainActivity 中就可以通过依赖注入的方式得到 MyViewModel 的实例了。! m+ j- r8 y0 v2 O* r2 h3 _
    当然如果有引入类似 Activity 扩展库 ktx:
    6 Z* N1 E8 v+ c3 k3 R // ktx implementation "androidx.activity:activity-ktx:1.1.0"那么上面 MainActivity 可修改如下:
    * n" O) s" R. y  k- E@AndroidEntryPointclass MainActivity : AppCompatActivity() {    // 此时无需声明 @Inject 注解    private val viewModel: MyViewModel by viewModels()    ...}以上就是 ViewModel 的依赖注入。
    6 E$ N6 h# S' p9 W9 U5 n本篇文章就介绍到这。
    * {8 B/ @5 `7 b  h0 i# c$ z! w( F" l参考链接:0 z$ X& n2 A9 E' p
    Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
    ( X' ]% F) ~6 C; p# u% u使用 Hilt 实现依赖项注入(官网)8 t+ {( M2 C8 o" f: \) J; ?' y
    从 Dagger 到 Hilt,谷歌为什么执着于让我们使用依赖注入?7 a: e, C4 R; g; _: |9 \3 [

    3 e: h* j) F: A. X0 s7 I# ZJava吧 收集整理 java论坛 www.java8.com
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    快速回复 返回顶部 返回列表