• Android Jetpack 中Hilt的使用


    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"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在项目中使用hilt。

    Step1:使用@HiltAndroidApp注解

    新建继承自Application的类并添加注解@HiltAndroidApp,触发 Hilt 的代码生成,其中包括可以使用依赖项注入的应用基类。应用容器是应用的父容器,这意味着其他容器可以访问其提供的依赖项。

    @HiltAndroidApp 
    class LogApplication : Application()
    
    • 1
    • 2

    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() { .... }
    
    • 1
    • 2

    Step3:使用hilt进行字段注入

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

    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject lateinit var logger: LoggerLocalDataSource
        @Inject lateinit var dateFormatter: DateFormatter
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再比如Step3中的logger。它与DateFormatter的区别在于它的构造参数是有参数的。那么对于这种情况,我们还需要告诉hilt如何获取LogDao的实例。也就是说如果LogDao能通过构造器构建的话,直接添加@Inject注解就可以了。但是这里的logDao是一个接口,而且它无法手动添加实现类(这个是Android room中的DAO)。所以我们需要使用其他的方式获取

    @Singleton
    class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    step4-condition2:用 @Provides 提供实例

    我们可以在 Hilt 模块中用 @Provides 注释函数,以告诉 Hilt 如何提供无法注入构造函数的 类型。hilt模块也就是用@Module @InstallIn 注释的类的使用。无法通过对构造器添加@Inject注解方式提供实例时通过@Module@InstallIn(指定作用域)来声明提供对象实例的方式。 这个Module是模块,我们需要使用模块向 Hilt 添加绑定,换句话说,就是告诉 Hilt 如何提供不同类型的实例。 在 Hilt 模块中,您需针对无法注入构造函数的类型(如项目中未包含的接口或类)添加绑定。例如 OkHttpClient - 您需要使用其构建器来创建实例。因为这里实际上是提供数据库操作,所以作用域应该是全局的,所以采用的是SingletonComponent。这里还有其他的component

    @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()
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    step4-condition3:用 @Binds 提供接口。

    对于接口我们不能使用构造函数注入。 要告诉 Hilt 对接口使用什么实现,可以在 Hilt 模块内的函数上使用 @Binds 注释。@Binds必须对抽象函数作出注释(因为该函数是抽象的,因此其中不包含任何代码,并且该类也必须是抽象的)。抽象函数的返回类型是我们要为其提供实现的接口(即 AppNavigator)。通过添加具有接口实现类型(即 AppNavigatorImpl)的唯一参数来指定实现。比如在MainActivity中我们依赖的接口

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var navigator: AppNavigator
        ....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所以对此我们需要新建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()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    step4-condition4:使用限定符

    要告诉 Hilt 如何提供相同类型的不同实现(多个绑定),可以使用限定符。它的定义其实就是注解。

    @Qualifier 
    annotation class InMemoryLogger 
    @Qualifier 
    annotation class DatabaseLogger
    
    • 1
    • 2
    • 3
    • 4

    要比如对log的增删查提供一套基于内存的实现方式,那么定义接口

    interface LogDataSource {
        fun addLog(msg: String)
        fun getAllLogs(callback: (List) -> Unit)
        fun removeLogs()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    基于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) -> Unit) {
            executorService.execute {
                val logs = logDao.getAll()
                mainThreadHandler.post { callback(logs) }
            }
        }
    
        override fun removeLogs() {
            executorService.execute {
                logDao.nukeTable()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    基于内存的实现如下

    @ActivityScoped
    class LoggerInMemoryDataSource @Inject constructor():LogDataSource {
        private val logs = LinkedList()
    
        override fun addLog(msg: String) {
            logs.addFirst(Log(msg, System.currentTimeMillis()))
        }
    
        override fun getAllLogs(callback: (List) -> Unit) {
            callback(logs)
        }
    
        override fun removeLogs() {
            logs.clear()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    基于上面介绍,使用接口时我们定义实现类如下

    @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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到我们定义了两个module,之所以不是一个module是因为两种实现的作用域不一样。而且在InMemory的@Binds方法上我们还加入了@ActivityScoped,这个是必须加入的,因为实现类中指定了作用域。同理在这儿我们还加入了自定义的注解InMemoryLogger,就是告诉hilt选择那种方式提供实例。如果不加限定符的话会报错。真正使用该接口时如下

    class ButtonsFragment : Fragment() {
        @InMemoryLogger
        @Inject lateinit var logger: LogDataSource
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到与Step3中的区别在于此处变量的类型为接口而不是具体的实现,其次加入了限定符。综上就是Hilt的基本使用

    Android 学习笔录

    Android 性能优化篇:https://qr18.cn/FVlo89
    Android 车载篇:https://qr18.cn/F05ZCM
    Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
    Android Framework底层原理篇:https://qr18.cn/AQpN4J
    Android 音视频篇:https://qr18.cn/Ei3VPD
    Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
    Kotlin 篇:https://qr18.cn/CdjtAF
    Gradle 篇:https://qr18.cn/DzrmMB
    OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
    Flutter 篇:https://qr18.cn/DIvKma
    Android 八大知识体:https://qr18.cn/CyxarU
    Android 核心笔记:https://qr21.cn/CaZQLo
    Android 往年面试题锦:https://qr18.cn/CKV8OZ
    2023年最新Android 面试题集:https://qr18.cn/CgxrRy
    Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
    音视频面试题锦:https://qr18.cn/AcV6Ap

  • 相关阅读:
    【雷达通信】Matlab实现广义自适应多项式窗函数
    从代码逻辑到场景实战,百度高级工程师带你解密PP-ChatOCR!
    42V转5V芯片——高效率、宽压输入范围和强大输出能力
    spring cloud kubernetes 本地开发环境搭建
    【算法笔记】01. 如何正确的学习和练习算
    GC垃圾回收算法
    从vue(v2.7.10)源码分析vue是如何收集依赖和触发依赖
    好用的vue项目插件
    win7系统使用高版本node.js
    vue通知(滚动)
  • 原文地址:https://blog.csdn.net/maniuT/article/details/132714544