重学Android Jetpack(九)—Hilt基本使用

2,340 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

简介

Hilt 是什么?

官方的定义是:

Hilt是Android的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手动依赖项注入要求您手动构造每个类及其依赖项,并借助容器重复使用和管理依赖项。

Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。Hilt在热门DI库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。如需了解详情,请参阅 Hilt 和 Dagger

所以,Hilt是一个对Dagger库的扩展。那Hilt对于Dagger的优势主要在于HiltJetpack库和Android Framework的类是集成在一起的,并删除了大部分样板代码,我们只要专注于定义和注入绑定的部分。

为什么需要使用依赖注入?

使用依赖注入有以下的优点:

  • 简化 Android 应用的 Dagger 相关基础架构。
  • 创建一组标准的组件和作用域,以简化设置、提高可读性以及在应用之间共享代码。
  • 提供一种简单的方法来为各种构建类型(如测试、调试或发布)配置不同的绑定。

Hilt依赖

hilt-android-gradle-plugin插件依赖,在项目级的build.gradle 文件中添加:

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

应用层级的app/build.gradle 文件中添加以下依赖项:

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

dependencies {

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

因为Hilt使用Java 8功能,需在项目中启用 Java 8,在 app/build.gradle 文件中添加:

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

Hilt常用注解介绍

Hilt常用注解主要是:@HiltAndroidApp@AndroidEntryPoint@Inject@Module@InstallIn@Binds@Provides@EntryPoint 等等。

Hilt 应用类@HiltAndroidApp

所有使用Hilt的应用都必须包含一个带有@HiltAndroidApp注释的Application类。

@HiltAndroidApp
class HiltApp : Application() {

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

主要添加了@HiltAndroidApp 会触发Hilt的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。生成的这一Hilt组件会附加到Application对象的生命周期,并为其提供依赖项。此外,它也是应用的父组件,这意味着,其他组件可以访问它提供的依赖项。 在HiltApp中设置好@HiltAndroidApp之后,就可以使用Hilt提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver等。

将依赖项注入Android类 @AndroidEntryPoint

Application类中设置了Hilt且有了应用级组件后,Hilt可以为带有@AndroidEntryPoint注释的其他Android类提供依赖项:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }
}

Hilt目前支持以下Android类:

  • Application(通过使用 @HiltAndroidApp
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

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

注意:在 Hilt 对 Android 类的支持方面还要注意以下几点:

  • Hilt仅支持扩展ComponentActivityonentActivity)的Activity,如AppCompatActivity
  • Hilt仅支持扩展androidx.Fragment的Fragment。
  • Hilt不支持保留的Fragment。

@Inject

@AndroidEntryPoint会为项目中的每个Android类生成一个单独的Hilt组件。这些组件可以从它们各自的父类接收依赖项,如组件层次结构中所述。

如需从组件获取依赖项,可以使用@Inject注释执行字段注入:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var analytics: AnalyticsAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
    }
}

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

Hilt 注入的类可以有同样使用注入的其他基类。如果这些类是抽象类,则它们不需要 @AndroidEntryPoint 注释。

为了执行字段注入,Hilt需要知道如何从相应组件提供必要依赖项的实例。绑定包含将某个类型的实例作为依赖项提供所需的信息。向Hilt提供绑定信息的一种方法是构造函数注入。在某个类的构造函数中使用 @Inject 注释,以告知Hilt如何提供该类的实例:

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { }

在一个类的代码中,带有注释的构造函数的参数即是该类的依赖项。在本例中AnalyticsService是 AnalyticsAdapter的一个依赖项。因此,Hilt还必须知道如何提供AnalyticsService的实例。

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

  • 构建并验证依赖关系图,确保没有未满足的依赖关系且没有依赖循环。
  • 生成它在运行时用来创建实际对象及其依赖项的类。

Hilt模块 @Module

有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用Hilt @ModuleHilt提供绑定信息。比如我们常用于创建依赖类的对象(例如第三方库OkHttpRetrofit等等),使用@Module注解的类,需要使用@InstallIn注解指定module的范围。

@Module
@InstallIn(SingletonComponent::class)
// 把NetworkModule绑定到HiltApp 的生命周期。
object NetworkModule {

}

@InstallIn

使用@Module注入的类,需要使用@InstallIn注解指定module的范围,例如使用 @InstallIn(HiltApp::class)注解的module会绑定到activity的生命周期上。 Hilt提供了以下组件来绑定依赖与对应的Android类的活动范围:

Hilt 提供的组件对应的 Android 类的活动范围
SingletonomponentApplication
ActivityRetainedComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView annotated with @WithFragmentBindings
ServiceComponentService

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

@Binds 注入接口实例

接口是无法通过构造方法进行注入的,需要通过在Hilt 模块内创建一个带有@Binds注释的抽象函数进行注入。@Binds 注释会告知Hilt在需要提供接口的实例时要使用哪种实现。

带有注释的函数会向Hilt提供以下信息:

  • 函数返回类型会告知Hilt函数提供哪个接口的实例。
  • 函数参数会告知Hilt要提供哪种实现。

看下面代码:

interface AnalyticsService {
  fun analyticsMethods()
}

// 构造函数也需要注入,因为Hilt需要知道如何提供AnalyticsServiceImpl的实例。
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

其中的Hilt模块AnalyticsModule带有@InstallIn(ActivityComponent::class)注释,这是表示要Hilt 将该依赖项注入ExampleActivity

@Provides 注入实例

除了接口无法通过构造函数注入实例外,还有第三方库如:RetrofitOkHttpClient 或 Room数据库等都是无法通过构造函数注入,这时就需要使用@Provides注入实例。看如下代码:

@Module
@InstallIn(SingletonComponent::class)
// 把NetworkModule绑定到HiltApp 的生命周期。
object NetworkModule {

    @Provides
    fun getApiService() : ApiService{
        return Retrofit.Builder()
            .baseUrl("https://www.wanandroid.com")
            .build()
            .create(ApiService::class.java)
    }
}

带有@Provides注释的函数会向 Hilt 提供以下信息:

  • 函数返回类型会告知Hilt函数提供哪个类型的实例;
  • 函数参数会告知Hilt相应类型的依赖项;
  • 函数主体会告知Hilt如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。

Hilt 中的预定义限定符

Hilt提供了一些预定义的限定符。例如,由于我们可能需要来自应用或ActivityContext类,因此Hilt提供了@ApplicationContext和 @ActivityContext 限定符。

假设下面例子中的AnalyticsAdapter类需要Activity的上下文。以下代码演示了如何向AnalyticsAdapter提供Activity上下文:

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { }

@EntryPoint注解

Hilt支持最常见的AndroidApplicationActivityFragmentViewServiceBroadcastReceiver 等等,但是我们可能需要在Hilt 不支持的类中执行依赖注入,在这种情况下可以使用@EntryPoint注解进行创建,Hilt会提供相应的依赖。

Hilt的基本使用

基本的Android组件中使用

先创建一个HiltSample类,并使用@Inject注解修饰构造方法,这时告诉Hilt如何提供该类的实例,@Inject常用于构造函数、非私有字段、方法中。

class HiltSample @Inject constructor() {

    companion object{
        val TAG = HiltSample::class.simpleName
    }

    fun hiltTodo(str: String){

        Log.e(TAG,str)
    }
}

创建Fragment并添加注解和过@Inject获取HiltSample实例:

/**
 * 注解 fragment后,它依赖的activity也需要注解
 */
@AndroidEntryPoint
class MainFragment : Fragment() {

    @Inject
    lateinit var hiltSample: HiltSample

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View? {
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        hiltSample.hiltTodo("MainFragment")
    }
}

Activity中使用MainFragment

/**
 * 为项目中的每个Android类生成一个Hilt组件,这些组件可以从它们各自的父类接收依赖项
 *
 * 抽象类不可使用@AndroidEntryPoint进行注解
 */

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

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

        supportFragmentManager.beginTransaction()
            .add(R.id.container, MainFragment::class.java, null)
            .commit()
    }
}

Hilt在第三方库中的使用

在项目中注入第三方库类组件的依赖,我们需要使用@Module注解,并在使用@Module注解的类中创建第三方依赖的对象。

@Module
@InstallIn(SingletonComponent::class) // 把NetworkModule绑定到HiltApp 的生命周期
object NetworkModule {

    @Provides
    @Singleton
    fun getOkHttpClient() : OkHttpClient{

        return OkHttpClient.Builder().build()

    }

    @Provides
    @Singleton
    fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://www.wanandroid.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun getApiService() : ApiService{
        return createRetrofit(getOkHttpClient()).create(ApiService::class.java)
    }
}

这里要注意两点:

  • 使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,会绑定到 Android 类对应的生命周期上。
  • @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

Hilt在ViewModel的使用

先添加ViewModel的依赖:

implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
kapt 'androidx.hilt:hilt-compiler:1.0.0'

透过使用@HiltViewModel添加注释, 并在ViewModel对象的构造函数中使用@Inject注释来提供一个ViewModel

@HiltViewModel
class HiltViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repository: HiltRepository,
) : ViewModel() {

}

然后,带有 @AndroidEntryPoint注释的ActivityFragment可以使用ViewModelProvider或 by viewModels() KTX扩展照常获取 ViewModel 实例:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  private val hiltViewModel: HiltViewModel by viewModels()
  ...
}

Hilt与Room的结合使用

Hilt中我们只要使用注解@Module创建 RoomModule文件,使用@InstallIn 注解指定module的生命周期,例如使用@InstallIn(SingletonComponent::class) 注解 module会绑定到Application的生命周期上:

@Module
@InstallIn(SingletonComponent::class)
// SingletonComponent把RoomModule 绑定到 Application的生命周期。
object RoomModule {

    @Provides
    @Singleton
    fun provideAppDataBase(application: Application): AppDataBase {
        return Room
            .databaseBuilder(application, AppDataBase::class.java, "dhl.db")
            .fallbackToDestructiveMigration()
            .allowMainThreadQueries()
            .build()
    }
}

总结

由于之前没有使用过Dagger,无法评判HiltDagger的优劣势,社区中好多同学说Dagger学习的成本非常高,而且难用,所以这也是为什么Android开发团队基于Dagger开发了Hilt这个框架。有一点比较显而易见的是,Hilt 集成了Jetpack库和Android框架类,并自动管理它们的生命周期,这让开发者只用关注如何绑定就好,的确方便了我们的学习和使用。这篇文章只是介绍学习了Hilt的概念和一些基本使用,后续会考虑在之前开源的WanAndroid项目用上。项目中的大多数知识点都来自官网,如有错误,烦请指正,谢谢。