好用的依赖注入框架-Hilt

6,009 阅读7分钟

为什么使用依赖注入

要学习某个框架,必须要弄明白它是用来干嘛的,有什么好处。 那么Hilt是什么呢,它有什么好处呢?

首先,Hilt是一个依赖注入框架。依赖就是一个对象的功能依赖于其他对象去实现。就比如我们要上网,那我们就依赖于手机或者电脑,而在项目中,ViewModel想要获取数据就依赖于数据仓库Repository。我们依赖于某个东西的功能去实现自己的需求,这就是依赖。

而想要使用某个对象的功能,通常是直接new一个对象出来然后使用它的功能即可。但是这样的话,当依赖的对象很多的话,会导致类本身非常臃肿。因为要保持对每个依赖对象的创建和维护,而我们仅仅是想要使用它的功能而已,对于其他的并不关心。因此可以提供一个方法setXXX,这样类本身并不去创建维护对象,而是交给外部去管理并传递进来,这就是注入。

将依赖对象的创建交给了外部去传递进来,那么这个外部又应该是谁呢?发现这个过程交给谁都不太合适,因此就单独创建一个容器去创建管理,这个容器就是依赖注入框架,也就是本章说的Hilt

添加Hilt依赖

首先在项目的build.gradle中加入如下代码:

buildscript {
    ext.hilt_version = "2.36"
    ...
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}
...

然后在要使用Hilt的modulebuild.gradle中加入:

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

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

dependencies {
    ...
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

前置工作

要使用Hilt,首先要对Application进行处理,使用@HiltAndroidApp去注解Application。这是必须要做的,使用这个注解后,Hilt才会去生成一系列的容器组件,这时候才能够使用Hilt。

// App.kt
@HiltAndroidApp
class App:Application()
// AndroidManifest.xml
<manifest>
    <application
        android:name=".App"
        ...>
    </application>
</manifest>

使用Hilt

@Inject标记注入

Hilt的注入是通过注解来进行标识的,要注入的对象使用@Inject注解即可。同样的,要注入的对象的构造方法也要使用@Inject注解,如下例。

// Person.kt
class Person @Inject constructor() {
    fun say() {
        println("Hello")
    }
}

// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var person: Person

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        person.say()
    }
}

MainActivity中并没有直接给person赋值,而是使用了@Inject进行注解,然后就直接使用了,这是因为Hilt在背后给这个字段去初始化赋值了。为了能够让Hilt去生成一个Person对象进行注入,我们还要给Person的构造方法上也加上@Inject,这样就完成了一个最简单的依赖注入了。

@AndroidEntryPoint注入的入口点

还有一点就是在MainActivity上还使用了@AndroidEntryPoint,这个注解表示当前的Activity是一个注入的入口点,可以进行注入。Hilt并不是在哪都能进行注入的,而是有着特定的入口点,并且入口点必须得通过@AndroidEntryPoint注释。其中入口点有6个,Application,Activity,Fragment,View,Service,BroadcastReceiver。但是Application这个入口点不用使用@AndroidEntryPoint注解,因为它已经有了@HiltAndroidApp,所以可以直接注入。

// App.kt
@HiltAndroidApp
class App:Application() {
    @Inject
    lateinit var person: Person

    override fun onCreate() {
        super.onCreate()
        person.say()
    }
}

这几个入口点中,ViewFragment有些特殊,其他的入口点只要注解为@AndroidEntryPoint后,即可在其中进行Hilt的注入。但是对于Fragment而言,若是想在Fragment中使用Hilt的注入,除了在Fragment上使用@AndroidEntryPoint外,还要在其宿主Activity上也加上这个注解才行。而对于View而言,若是在View中使用Hilt的注入,首先在View上使用@AndroidEntryPoint,然后若是View用在Activity上,则Activity上也要加该注解。若是View用在Fragment上,则Fragment和Fragment的宿主Activity都要加上这个注解

可以看到Hilt的使用是比较简单的,首先将类的构造方法使用@Inject注解,这表明该类可以被Hilt自动创建并注入到相应的地方,然后就是在入口点中使用@Inject进行注入即可。

@HiltViewModel 注入ViewModel

ViewModel的注入和普通对象一样,首先给构造方法加@Inject,但是比普通对象多出来的是还要在它上面加入@HiltViewModel。并且注入的地方不能使用@Inject,而是和普通的使用ViewModel保持一致,使用ViewModelProvider去获取ViewModel。

注意这里不能使用@Inject去注入ViewModel,否则获取到的ViewModel只是一个普通对象,它在Activity销毁的时候也会被回收,而无法做到如ViewModel那样的在配置改变的时候依旧保存下来。

// MainAcitivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: ViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this)[MyViewModel::class.java]
        println(viewModel)
    }
}

// MyViewModel.kt
@HiltViewModel
class MyViewModel @Inject constructor() : ViewModel()

如是嫌弃ViewModelProvider方式获取的太麻烦,则可以使用Activity-ktx的获取方式:

// build.gradle中加入依赖
implementation 'androidx.activity:activity-ktx:1.2.3'


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val  viewModel:MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

// 若是想要在Fragment中也这样使用,需要加入Fragment-ktx的依赖:
implementation 'androidx.fragment:fragment-ktx:1.3.4'

对于无法在构造方法上加@Inject的类如系统类三方库中的类等,是不能直接进行注入的,要通过安装模块的方式去添加依赖。

@Module @InstallIn 声明一个模块

模块也就是Module,是一个类文件,它包含了很多的方法,这些方法就是用来提供注入对象的。

模块必须使用@Module来进行注解,说明当前类是一个Hilt模块,可以用来提供依赖。并且同时还要使用@InstallIn注解,该注解接收一个数组类型的参数,表示安装在哪个组件上。

组件代表着一个作用范围,安装在该组件上的模块所提供的依赖方法,只能在当前组件范围内才能进行注入。而且不同的组件对应着不同的生命周期,安装在它上面的模块只会在其生命周期内存在。

// NetModule.kt
@InstallIn(SingletonComponent::class)
@Module
object NetModule {

    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://xxxx.com/")
            .build()
    }
}

如上例,就是声明了一个NetModule模块,用来提供Retrofit的依赖,并且安装在SingletonComponent组件上,SingletonComponent组件的作用范围是全局,因此在所有的地方都能使用该模块所提供依赖注入,也就是对Retrofit的注入。

在这个模块中,有个@Provide标注的方法,该注解表明这个方法是是用来提供依赖的。注意它的返回值是Retrofit,表明需要注入Retrofit实例的时候,就会通过这个方法去生成一个实例对象进行注入。在Module中,Module的类名,方法名都是随意定的,Hilt只关心返回值。下面就是可以直接在Activity中注入Retrofit了:

// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var retrofit: Retrofit

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println(retrofit.hashCode())
    }
}

组件Component

当声明一个module的时候,必须要安装在组件上,这代表当前module可以给哪些类进行注入。在Hilt中,一共有种组件,下面将逐一介绍组件的生命周期和作用范围。

SingletonComponent

SingletonComponent是针对Application的组件,安装在它上面的module会与Application的生命周期保持一致,在Application#onCreate的时候创建,在Application#onDestroy的时候销毁。并且该module所提供的依赖,在整个程序中都是可以使用的。

下面声明了两个模块,都是安装在SingletonComponent上的,但是一个是普通类,一个是单例类:

@Module
@InstallIn(SingletonComponent::class)
class NormalModule {...}

@Module
@InstallIn(SingletonComponent::class)
object SingletonModule {...}

这两种类型的模块有什么区别呢?使用class关键字的类是一个普通对象,因此会存在创建和销毁。它存在的范围也就是前面所说的组件的生命周期。例如在SingletonSomponent组件上,会在Application#onCreate的时候去new出一个NormalModule的实例对象,在Application#onDestroy的时候回收这个对象。

注意他是每个组件都会生成一个Module对象实例,例如若是这个module安装在ActivityComponent上的时候,会在每个Activity#onCreate的时候去创建一个module实例,也就是说,每个Activity对应的module都是独立的对象

而使用object关键字的话,声明出来的module是一个单例对象,因此不会存在创建销毁过程。提供依赖的时候,用的都是同一个单例对象。

ActivityRetainedComponent

ActivityRetainedComponent是针对Activity的组件,因此它的生命周期是Activity#onCreateActivity#onDestroy,但是它又比这个范围长一些。也就是当配置更改的时候,如旋转屏幕导致的Activity重建的时候,该组件并不会销毁,而是真正结束一个Activity的时候才会去销毁。简单来说就是生命周期与ViewModel是一致的。

安装在ActivityRetainedComponent组件上的module提供的依赖,可以在ViewModel中,Activity中,Fragment中,以及View中注入。

ActivityComponent

ActivityComponent组件的生命周期也是与Activity一致,但是,它是跟Activity完全一致的。只要Activity销毁,对应的组件也会销毁。

安装在ActivityComponent上的module提供的依赖,可以在Activity中,Fragment中,以及View中注入

ViewModelComponent

ViewModelComponent组件和ActivityRetainedComponent是一样的,声明周期也是与ViewModel一致。唯一的区别是,安装在它上面的模块提供的依赖只能在ViewModel中使用

FragmentComponent

FragmentComponent组件是针对于Fragment的,安装在它上面的组件在Fragment#onAttach的时候创建,在Fragment#onDestroy的时候销毁。

安装在它上面的module提供的依赖,只能在Fragment中注用

ViewComponent

ViewComponent组件是针对于View的,在View创建的时候创建,在视图销毁的时候销毁。并且安装在它上面的module提供的依赖只能在View中使用

ViewWithFragmentComponent

ViewWithFragmentComponent也是针对View的,但是注入的时候不仅要求在View上加入@AndroidEntryPoint,还要加上@WithFragmentBindings。安装在它上面的模块的生命周期也是与ViewComponent一样的。其中提供的依赖只能用在View上而且这个View还只能用在Fragment中,不能用在Activity中

ServiceComponent

ServiceComponent组件是针对Service的,依附于它的module在Service#onCreate的时候创建,在Service#onDestroy的时候销毁。并且安装在它上面的module只能用在Service中

七种组件的module生命周期以及使用范围,仅适用class关键字的module
组件module创建module销毁可使用的入口点
SingletonComponentApplication#onCreateApplication#onDestroy全部
ActivityRetainedComponentActivity#onCreateActivity#onDestroyViewModel,Activity,Fragment,View
ActivityComponentActivity#onCreateActivity#onDestroyActivity,Fragment,View
ViewModelComponentActivity#onCreateActivity#onDestroyViewModel
FragmentComponentFragment#onAttachFragment#onDestroyFragment
ViewComponentView创建View销毁View
ViewWithFragmentComponentView创建View销毁View
ServiceComponentService#onCreateService#onDestroyService

作用范围很好理解,是用来缩小注入的范围,以避免滥用注入。那么生命周期又有什么用呢?比如在MainActivity中有三个Fragment,这三个Fragment想要共享一个对象Person,那么该怎么实现呢?

第一种方法是定义在Activity中,然后通过Fragment拿到Activity,进而拿到这个Person对象。第二个方法是将Person对象放到Activity的ViewModel中,然后在Fragment中也去获取这个ViewModel,进而拿到Person对象。

最后一种方式就是利用Hilt生命周期的特性:

@Module
@InstallIn(ActivityComponent::class)
class ShareModule {
    private val person = Person()

    @Provides
    fun providePerson(): Person {
        return this.person
    }

}

首先我们知道,ActivityComponent上的module的在Activity#onCreate的时候创建,在Activity#onDestroy的时候销毁。因此它是与Activity对应的,而这个module提供的依赖是ShareModule中的内部对象。因此,只要Activity没有销毁,这个module也就是同一个对象,进而注入的依赖person也都是同一个对象,从而实现Fragment共享同一个对象。这时候只要在Fragment中这样使用就行了:

@AndroidEntryPoint
class MyFragment : Fragment(){
    @Inject
    lateinit var person: Person
    ...
}
作用域

除了使用上述的方式可以实现在Activity的生命周期内共享某个依赖对象外,Hilt还提供了一个作用域的概念。在某个作用域内,提供的依赖对象也是唯一的,将上例中的ShareModule改造一下:

@Module
@InstallIn(ActivityComponent::class)
object ShareModule {

    @ActivityScoped
    @Provides
    fun providePerson(): Person {
        return Person()
    }

}

改造后的module也能实现和前面那样的效果,在Activity的生命周期内提供相同的Person对象。而只是简单的加了一个@ActivityScoped注解,这样,在Activity的生命周期范围内,拿到的依赖对象仍然是同一个,即使将module类使用object关键字声明成了单例类。

可以看到,使用作用域注解可以实现基于组件生命周期内提供单一对象的功能,这样的话,就可以直接将module定义为单例类就行了,若是java中的话定义成静态方法即可,这样可以用来避免频繁创建对象导致的开销。

另外注意一点就是,作用域注解必须与组件注解保持一致,比如在ActivityComponent只能使用ActivityScoped作用域,作用域注解的提供依赖的方法,在组件的生命周期内提供的是同一个依赖对象。

作用域注解不只是在module中使用,直接在类上面加上也是可以的,使用下面的代码也可以实现上述的效果:

@ActivityScoped
class Person @Inject constructor() {...}
对应Component的作用域
组件生命周期生命周期作用域
SingletonComponentApplication#onCreateApplication#onDestroySingleton
ActivityRetainedComponentActivity#onCreateActivity#onDestroyActivityRetainedScope
ActivityComponentActivity#onCreateActivity#onDestroyActivityScoped
ViewModelComponentActivity#onCreateActivity#onDestroyViewModelScoped
FragmentComponentFragment#onAttachFragment#onDestroyFragmentScoped
ViewComponentView创建View销毁ViewScoped
ViewWithFragmentComponentView创建View销毁ViewScoped
ServiceComponentService#onCreateService#onDestroyServiceScoped

在定义模块的时候,使用@InstallIn可以限定当前模块安装在哪个组件上。其中@InstallIn的参数是个数组,也就是说,我们可以将这个模块安装在多个组件上。使用多个组件可以将作用范围进行扩大,比如使用FragmentComponentServiceComponent,就可以使模块中的依赖在FragmentService中使用了。但是,使用了多组件的话,因为组件的生命周期和作用范围不同,因此是不能声明作用域注解的

当然若是两个组件的生命周期是一样的,比如ViewComponentViewWithFragmentComponent,则还是可以使用作用域注解@ViewScoped的,但是这没有什么意义,因为ViewComonent的范围是包含了Fragment的。

@Provide和@Binds

@Provide前面有说过了,是在Module中用来修饰方法的,被它修饰的方法代表着提供依赖的方法,当需要该类型的依赖对象时,就会调用对应返回值的方法去注入依赖。

在Module中,类名和方法名都是没有意义的,可以随便起名(当然为了可读性还是不要随便起名),至于提供什么依赖完全看函数的返回值类型。若是返回值类型是一个接口呢?。

举个例子,比如有个接口Human,两个子类Man和Woman

interface Human {
    fun sex(): String
}

class Man @Inject constructor() : Human {
    override fun sex(): String {
        return "男"
    }
}

class Woman @Inject constructor() : Human {
    override fun sex(): String {
        return "女"
    }
}

若是想要在Activity中注入该怎么处理呢,简单,构造方法已经加入注解了,然后直接注入即可:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var man: Man

    @Inject lateinit var woman: Woman

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println("Man:${man.sex()}, Woman: ${woman.sex()}")
    }
}

那我要是想要在Activity中注入Human而不是具体的子类型,该怎么办呢?首先我们知道,Hilt注入的时候是根据类型来查找依赖关系的。顺序是先从当前组件上的Module上查找返回值类型为这个类型的方法,找不到后再去看这个类型的类上的构造方法上是否有@Inject注解,有的话就直接生成一个对象注入了。

而若是注入类型为Human的,因为Human是个接口,是没有构造方法的,因此也是没法去@Inejct的。因此,若是想注入一个接口类型,必须要为它提供一个module。

@Module
@InstallIn(ActivityComponent::class)
object HumanModule {

    @Provides
    fun provideMan():Human {
        return Man()
    }
}

ActivityComponent上安装这个Module后,就可以在Activity中使用Human注入了。另外这里注意一点,Module一般是用来提供不可以直接注入的对象,也就是三方库系统类那样的无法在构造方法添加@Inject的类,对于我们自己的类,如上面的Man对象,则不要在provideMan方法中直接去new一个对象,而是应该使用注入的方式,如下:

@Module
@InstallIn(ActivityComponent::class)
object HumanModule {
    
    @Provides
    fun provideMan(man:Man):Human {
        // 方法参数中的参数,会被Hilt直接注入
        // 所以若是提供依赖的方法含有参数的话,参数必须是能够被注入的,否则会报错
        return man
    }
}


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var man: Human

    @Inject lateinit var woman: Human

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        println("Man:${man.sex()}, Woman: ${woman.sex()}")
    }
}

这样,在Activity中虽然可以注入Human了,但是注入的两个对象man和woman实际类型都是Man。若是想要它们不一样该怎么处理呢?

@Module
@InstallIn(ActivityComponent::class)
object HumanModule {

    @Provides
    fun provideMan(man:Man):Human {
        return man
    }

    @Provides
    fun provideWoman(woman: Woman):Human {
        return woman
    }
}

如上面的这个代码,直接再加一个方法,提供Woman对象,这样可以吗?当然是不行的,Hilt是根据返回值类型来选择使用哪个方法去生成依赖对象的。这两个方法的返回值都是Human,会导致注入的时候不知道该用哪个,因此这样写在编译的时候就会直接报错了。

Qualifier

@Qualifier就是用来解决上述问题的,当moudle中的两个或多个方法返回的类型是同样的时候,就代表着有了依赖冲突,肯定是编译不过的。因此就需要解决冲突,而解决冲突的方式就是给它们添加限定符,这样就可以将它们区分开了。 注意注入的时候也要加上限定符:

// 定义两个注解,这两个注解用@Qualifier注解,代表着两种限定符
@Qualifier
annotation class ManType

@Qualifier
annotation class WomanType

// 给module中重复的类型添加限定符
@Module
@InstallIn(ActivityComponent::class)
object HumanModule {
    @ManType
    @Provides
    fun provideMan(man:Man):Human {
        return man
    }

    @WomanType
    @Provides
    fun provideWoman(woman: Woman):Human {
        return woman
    }    
}

// 使用的时候也要加上限定符
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @ManType
    @Inject
    lateinit var man: Human

    @WomanType
    @Inject
    lateinit var woman: Human

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
    }
}

可以看到上例中,有两个方法返回的都是Human,但是通过ManTypeWomanType注解进行了区分,在Activity中注入的时候也是要进行区分的,否则仍然会导致编译失败。若是在注入的时候不想使用Qualifier,那么可以在Module中再增加一个方法,不加以任何修饰,这样就可以使用默认的了。

@Module
@InstallIn(ActivityComponent::class)
object HumanModule {
    @ManType
    @Provides
    fun provideMan(man: Man): Human {
        return man
    }

    @WomanType
    @Provides
    fun provideWoman(woman: Woman): Human {
        return woman
    }

    @Provides
    fun provideDefault(man: Man): Human {
        return man
    }
}

// 在Activity中的这三种注入,分别对应上面module中的方法
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @ManType
    @Inject
    lateinit var man: Human // Man类型,provideMan方法提供

    @WomanType
    @Inject
    lateinit var woman: Human // Woman类型,provideWoman方法提供

    @Inject
    lateinit var default: Human // Man类型,provideDefault方法提供

    ...
}

上述就是Qualifier方法的使用情景,从开始到现在我们都是在module中使用@Provides去提供依赖,实际上还可以通过@Binds去提供依赖,这里改一下module:

@Module
@InstallIn(ActivityComponent::class)
abstract class HumanModule {
    @ManType
    @Binds
    abstract fun provideMan(man: Man): Human

    @WomanType
    @Binds
    abstract fun provideWoman(woman: Woman): Human

    @Binds
    abstract fun provideDefault(man: Man): Human
}

可以看到和使用Provides的区别就是:module必须是一个抽象类@Provides换成了@Binds,提供依赖的方法必须是一个抽象方法,这个抽象方法只能有一个参数必须有返回值。这里注意的是提供依赖的方法是个抽象方法,返回值类型是提供的依赖类型,而参数就是实际返回的依赖对象。因此,@Binds仅适用于返回父类型的情况,所以抽象方法的返回值类型必须是参数的父类型。

相同类型的话仍然是通过Qualifier去进行区分的,所以@Provides方式的module是可以完全取代@Binds方式的。

这里举例是用接口举例的,实际上对于不是接口的也是可以的,甚至是同一个对象也是没问题的。因为Hilt会将限定符和返回值作为一组判定,只要不发生重复即可,所以返回值是接口还是父类都无关紧要的,如下这样也是可以的:

@Module
@InstallIn(SingletonComponent::class)
object NetModule {
    
    @MyRelease
    @Provides
    fun provideRelease():Retrofit {
        return Retrofit.Builder()
            .baseUrl("http://www.xxx.com/")
            .build()
    }
    
    @MyTest
    @Provides
    fun provideTest() :Retrofit {
        return Retrofit.Builder()
            .baseUrl("http://test.com/")
            .build()
    }
}

@Qualifier
annotation class MyTest
@Qualifier
annotation class MyRelease

其他

其他默认提供的依赖,常用的一个Application,两种Activity,两种Context

    // Application,实际类型是我们自定义的App
    @Inject
    lateinit var mApp:Application
    
    // 在同一个Activity种,这种两注解拿到的都是同一个对象
    @Inject
    lateinit var activity:Activity
    @Inject
    lateinit var fragmentActivity: FragmentActivity
    
    // 这是Application对应的Context,使用限定符@ApplicationContext区分
    @ApplicationContext
    @Inject
    lateinit var mAppContext: Context
    
    // 这是Activity对应的Context,使用限定符@ActivityContext区分
    @ActivityContext
    @Inject
    lateinit var mContext: Context

可以看到,Hilt的入口点和组件息息相关。而除了这些入口点外,还能自定义入口点,但是意义不大,因为Hilt定义的入口点基本已经覆盖了Android中比较重要的东西了。所以自定义入口点意义不大。

附页

该部分内容是回复评论区的朋友添加的:

为什么Hilt可以用原生的方式去创建ViewModel

Hilt对于ViewModel的创建方式和原生的创建方式是一致的,唯一的差别就是Hilt中在ViewModel加入了@HiltViewModel@Inject两个注解。

原生有以下两种方式创建:

// 方式一,使用activity-ktx提供的委托机制(还有fragment-ktx)
private val viewModel by viewModels<MyViewModel>()
    
// 方式二,使用最基本的获取方式
viewModel = ViewModelProvider(this)[MyViewModel::class.java]

其实这两种方式实际都是一样的原理,都是通过getDefaultViewModelProviderFactory()方法去创建一个ViewModelProvider.Factory,然后由这个Factory去创建ViewModel

所以,若是想要像正常那样创建ViewModel,则必须要重写getDefaultViewModelProviderFactory方法。实际上,编译时Hilt会根据注解去对于使用@AndroidEnterPointer注解的入口点类生成一个父类,然后通过字节码插桩方式去将该类的父类改为Hilt生成的类。这里这个类就是Hilt_MainActivity

public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
  ...
  @Override
  public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
    return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
  }
}

而在Hilt_MainActivity中也能看到,它确实重写了getDefaultViewModelProviderFactory

注意第二个参数是super.getDefaultViewModelProviderFactory,这是原本的Factory。沿着getActivityFactory继续追踪下去,最终会走到HiltViewModelFactory中去,这个类也是Factory的实现类。

public final class HiltViewModelFactory implements ViewModelProvider.Factory {
  ...
  @NonNull
  @Override
  public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    if (hiltViewModelKeys.contains(modelClass.getName())) {
      return hiltViewModelFactory.create(modelClass);
    } else {
      return delegateFactory.create(modelClass);
    }
  }
}

只看我们关注的部分,也就是Factory接口的实现方法create。从中也可以看出,当hiltViewModelKeys中包含当前要创建的ViewModel的类名的时候,使用hiltViewModelFactory去创建,否则使用delegateFactory去创建。

其中delegateFactory就是前面说的那个super.getDefaultViewModelProviderFactory,也就是原本的Factory。当ViewModel加了@HiltViewModel注解后,Hilt就会为它生成一个名字叫做原类名_HiltModules的类,并且有个静态的provide方法,该方法返回ViewModel的完整类名:

public final class MyViewModel_HiltModules {
    ...
    @Provides
    @IntoSet
    @HiltViewModelMap.KeySet
    public static String provide() {
      return "com.example.myapplication.MyViewModel";
    }
  }
}

而前面HiltViewModelFactory中的hiltViewModelKeys就是调用每个xxx_HiltModules#provide方法形成的Set集合。所以到这里就很清晰了:

  • 若是ViewModel使用了@HiltViewModel注解,就会使用hiltViewModelFactory去创建实例。

  • 若是没有使用,则使用delegateFactory(也就是默认的Factory)去创建实例。


所以,ViewModel上是否使用@HiltViewModel都是能正常运行的。但是:

  • <不使用注解> 默认的Factory只能创建空参构造方法ViewModel

  • <使用注解> Hilt的Factory可以创建带参数构造方法ViewModel,当然参数必须也是可以进行注入的。