Hilt 是 Android 的依靠项注入库,可减少在项目中履行手动依靠项注入的样板代码。履行手动依靠项注入要求您手动结构每个类及其依靠项,并凭借容器重复运用和办理依靠项。

Hilt 经过为项目中的每个 Android 类供给容器并主动办理其生命周期,供给了一种在运用中运用 DI(依靠项注入)的标准办法。Hilt 在抢手 DI 库Dagger的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和Android Studio 支撑。本篇只探讨其运用办法,其步骤如下

在项目中引入Hilt。

project/build.gradle下参加kotlin和hilt的插件

buildscript {
    ext.kotlin_version = '1.5.31'
    ext.hilt_version = '2.40'
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.3'
        //kotlin编译插件
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //hilt编译插件
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

app/build.gradle下参加kotlin和hilt

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-parcelize'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
android {
    compileSdkVersion 31
    buildToolsVersion "30.0.3"
    defaultConfig {
        applicationId "com.example.android.hilt"
        minSdkVersion 16
        targetSdkVersion 31
        versionCode 1
        versionName "1.0"
        javaCompileOptions {
            annotationProcessorOptions {
                arguments["room.incremental"] = "true"
            }
        }
    }
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    // Room
    implementation "androidx.room:room-runtime:2.3.0"
    kapt "androidx.room:room-compiler:2.3.0"
    // Hilt dependencies
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

在项目中运用hilt。

Step1:运用@HiltAndroidApp注解

新建承继自Application的类并增加注解@HiltAndroidApp,触发 Hilt 的代码生成,其中包括能够运用依靠项注入的运用基类。运用容器是运用的父容器,这意味着其他容器能够拜访其供给的依靠项。

@HiltAndroidApp
class LogApplication : Application()

Step2:运用@AndroidEntryPoint将依靠注入Android类。

在Application类中设置了 Hilt 且有了运用级组件后,Hilt 能够为带有@AndroidEntryPoint注解的其他 Android 类供给依靠项。Hilt 现在支撑以下 Android 类:

  • Application(经过运用 @HiltAndroidApp)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

假如您运用 @AndroidEntryPoint 为某个 Android 类增加注解,则还有必要为依靠于该类的 Android 类增加注解。例如,假如您为某个 Fragment 增加注解,则还有必要为运用该 Fragment 的一切 Activity 增加注解。

@AndroidEntryPoint
class LogsFragment : Fragment() { .... }

Step3:运用hilt进行字段注入

@Inject 注解让 Hilt 注入不同类型的实例。其实便是声明变量的时分用上这个注解

@AndroidEntryPoint
class LogsFragment : Fragment() {
    @Inject lateinit var logger: LoggerLocalDataSource
    @Inject lateinit var dateFormatter: DateFormatter
    ...
}

Step4:Hilt供给实例。

step4-condition1:在结构器上运用@Inject获取实例。

关于用@Inject注解的变量,供给其实例时,假如是经过结构器创建的实例那么咱们能够直接在结构器上运用@Inject注解就能够让hilt为咱们创建类的实例,比方下面的DateFormatter

/**
 * 经过结构器创建依靠
 */
class DateFormatter @Inject constructor() {
    @SuppressLint("SimpleDateFormat")
    private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")
    fun formatDate(timestamp: Long): String {
        return formatter.format(Date(timestamp))
    }
}

再比方Step3中的logger。它与DateFormatter的差异在于它的结构参数是有参数的。那么关于这种情况,咱们还需求告知hilt怎么获取LogDao的实例。也便是说假如LogDao能经过结构器构建的话,直接增加@Inject注解就能够了。可是这儿的logDao是一个接口,并且它无法手动增加完成类(这个是Android room中的DAO)。所以咱们需求运用其他的办法获取

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
    ...
}

step4-condition2:用 @Provides 供给实例

咱们能够在 Hilt 模块中用@Provides注释函数,以告知 Hilt 怎么供给无法注入结构函数的 类型。hilt模块也便是用@Module@InstallIn注释的类的运用。无法经过对结构器增加@Inject注解办法供给实例时经过@Module@InstallIn(指定效果域)来声明供给对象实例的办法。 这个Module是模块,咱们需求运用模块向 Hilt 增加绑定,换句话说,便是告知 Hilt 怎么供给不同类型的实例。 在 Hilt 模块中,您需针对无法注入结构函数的类型(如项目中未包括的接口或类)增加绑定。例如 OkHttpClient – 您需求运用其构建器来创建实例。由于这儿实际上是供给数据库操作,所以效果域应该是大局的,所以选用的是SingletonComponent。这儿还有其他的component

Android中Hilt的使用

@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
//这个能够是个class,可是在 Kotlin 中,只包括 @Provides 函数的模块能够是 object 类。
//这样,供给程序即会得到优化,并几乎能够内联在生成的代码中。
    /**
     * 用 @Provides 供给实例。咱们能够在 Hilt 模块中用 @Provides 注释函数,
     * 以告知 Hilt 怎么供给无法注入结构函数的 类型。
     */
    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
//
        return database.logDao()
        //Hilt 可从上述代码中得知,在供给 LogDao 的实例时需求履行 database.logDao()。
        //由于咱们具有 AppDatabase 作为传递依靠项,因而咱们还需求告知 Hilt 怎么供给这种类型的实例。
    }
    //由于咱们一向希望 Hilt 供给相同的数据库实例,所以咱们用 @Singleton 注释 @Provides provideDatabase 办法。
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context):AppDatabase{
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }
}

step4-condition3:用 @Binds 供给接口。

关于接口咱们不能运用结构函数注入。 要告知 Hilt 对接口运用什么完成,能够在 Hilt 模块内的函数上运用 @Binds 注释。@Binds有必要对笼统函数作出注释(由于该函数是笼统的,因而其中不包括任何代码,并且该类也有必要是笼统的)。笼统函数的返回类型是咱们要为其供给完成的接口(即 AppNavigator)。经过增加具有接口完成类型(即 AppNavigatorImpl)的仅有参数来指定完成。比方在MainActivity中咱们依靠的接口

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var navigator: AppNavigator
    ....
}

所以对此咱们需求新建module运用@Binds获取,假如类型有效果域,则@Binds办法有必要有效果域注释

//咱们的新导航信息(即 AppNavigator)需求特定于 Activity 的信息
//(由于 AppNavigatorImpl 具有 Activity 作为依靠项)。
// 因而,咱们有必要将其安装在 Activity 容器中,而不是安装在 Application 容器中,由于这是有关 Activity 的信息地点。
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
    @Binds
    abstract fun provideNavigator(impl: AppNavigatorImpl):AppNavigator
    //参数为具体的完成类,所以要告知hilt怎么供给完成类的实例。下面的完成类经过结构函数供给实例
}
//======AppNavigatorImpl.ktx========//
//AppNavigatorImpl 会依靠于 FragmentActivity。由于系统会在 Activity 容器中供给 AppNavigator 实例
// (亦可用于 Fragment 容器和 View 容器,由于 NavigationModule 会安装在 ActivityComponent 中),所以 FragmentActivity 现在可用
class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator {
    override fun navigateTo(screen: Screens) {
        val fragment = when (screen) {
            Screens.BUTTONS -> ButtonsFragment()
            Screens.LOGS -> LogsFragment()
        }
        activity.supportFragmentManager.beginTransaction()
            .replace(R.id.main_container, fragment)
            .addToBackStack(fragment::class.java.canonicalName)
            .commit()
    }
}

step4-condition4:运用限定符

要告知 Hilt 怎么供给相同类型的不同完成(多个绑定),能够运用限定符。它的界说其实便是注解。

@Qualifier
annotation class InMemoryLogger 
@Qualifier 
annotation class DatabaseLogger

要比方对log的增删查供给一套根据内存的完成办法,那么界说接口

interface LogDataSource {
    fun addLog(msg: String)
    fun getAllLogs(callback: (List<Log>) -> Unit)
    fun removeLogs()
}

根据Room的完成如下,其实便是开篇说到的完成,只不过完成了该接口

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao):LogDataSource {
    private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
    private val mainThreadHandler by lazy {
        Handler(Looper.getMainLooper())
    }
    override fun addLog(msg: String) {
        executorService.execute {
            logDao.insertAll(
                Log(
                    msg,
                    System.currentTimeMillis()
                )
            )
        }
    }
    override fun getAllLogs(callback: (List<Log>) -> Unit) {
        executorService.execute {
            val logs = logDao.getAll()
            mainThreadHandler.post { callback(logs) }
        }
    }
    override fun removeLogs() {
        executorService.execute {
            logDao.nukeTable()
        }
    }
}

根据内存的完成如下

@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor():LogDataSource {
    private val logs = LinkedList<Log>()
    override fun addLog(msg: String) {
        logs.addFirst(Log(msg, System.currentTimeMillis()))
    }
    override fun getAllLogs(callback: (List<Log>) -> Unit) {
        callback(logs)
    }
    override fun removeLogs() {
        logs.clear()
    }
}

根据上面介绍,运用接口时咱们界说完成类如下

@Module
@InstallIn(SingletonComponent::class)
abstract class LoggingDatabaseModule {
    @DatabaseLogger
    @Binds
    @Singleton
    abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LogDataSource
}
@Module
@InstallIn(ActivityComponent::class)
abstract class LoggingInMemoryModule {
    @InMemoryLogger
    @Binds
    @ActivityScoped
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LogDataSource
}

能够看到咱们界说了两个module,之所以不是一个module是由于两种完成的效果域不一样。并且在InMemory的@Binds办法上咱们还参加了@ActivityScoped,这个是有必要参加的,由于完成类中指定了效果域。同理在这儿咱们还参加了自界说的注解InMemoryLogger,便是告知hilt选择那种办法供给实例。假如不加限定符的话会报错。真正运用该接口时如下

class ButtonsFragment : Fragment() {
    @InMemoryLogger
    @Inject lateinit var logger: LogDataSource
    ...
}

能够看到与Step3中的差异在于此处变量的类型为接口而不是具体的完成,其次参加了限定符。综上便是Hilt的基本运用