Hilt

Posted by ooftf on April 12, 2021

Hilt

优点

  1. 重用代码
  2. 易于重构
  3. 易于测试

缺点

  • 错误信息比较难理解

  • 上手难度非常高

Hilt 官方文档

依赖注入DI文档

dagger hilt 官方文档

构建并验证依赖关系图,确保没有未满足的依赖关系且没有依赖循环。

集成配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ext.hilt_version = "2.38.1"

classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// 所有模块都要添加下面的 plugin  和 引用和 compiler
plugins {
    id 'dagger.hilt.android.plugin'
}

implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
// When using Kotlin.
kapt 'androidx.hilt:hilt-compiler:1.0.0'

Activity 需要向ViewModel传参Id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TwoViewModel
@AssistedInject constructor(@Assisted val id: String, okHttpClient: OkHttpClient) :
    BaseViewModel(AppHolder.app) {

    @dagger.assisted.AssistedFactory
    interface AssistedFactory {
        fun create(pokemonName: String): TwoViewModel
    }

    companion object {
        fun provideFactory(
            assistedFactory: AssistedFactory,
            id: String
        ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return assistedFactory.create(id) as T// 这里 create 每次都是创建不同的对象
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
@AndroidEntryPoint
class TwoActivity : BaseMvvmActivity<ActivityTwoBinding, TwoViewModel>() {

    val id = "123"
    @Inject
    lateinit var factory: TwoViewModel.AssistedFactory
    override fun getViewModelFactory(): ViewModelProvider.Factory? {
        return TwoViewModel.provideFactory(factory, id)
    }

}

这种方式实质不是单独为 ViewModel 所提供的,为的是一些构造数据里面有String 等非特定类型参数的时候用 Assisted 来解决,这种方式由于有变动参数,所以无法设置 Scope ,AssistedFactory 可以设定 Scope 。

Inject 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface AnalyticsService {
    fun analyticsMethods()
}



class AnalyticsServiceImpl @Inject constructor():AnalyticsService {
    override fun analyticsMethods() {
        Log.e("analyticsMethods","analyticsMethods")
    }
}


@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
    //@Binds
    //abstract fun bindAnalyticsService(analyticsServiceImpl: AnalyticsServiceImpl): AnalyticsService
    @Binds
    abstract fun bindAnalyticsService2(analyticsServiceImpl: AnalyticsServiceImpl2): AnalyticsService
}

注解

component

组件 作用域 父组件
ActivityComponent ActivityScoped ActivityRetainedComponent
ActivityRetainedComponent ActivityRetainedScoped SingletonComponent
FragmentComponent FragmentScoped ActivityComponent
ServiceComponent ServiceScoped SingletonComponent
ViewComponent ViewScoped ActivityComponent
ViewModelComponent ViewModelScoped ActivityRetainedComponent
ViewWithFragmentComponent ViewScoped FragmentComponent
SingletonComponent Singleton

在 Hilt 中 scoped 和 component 是一一绑定的,例如:如果 module 装载到 SingletonComponent 上,那么其内部就只能使用 Singleton 作用域

scopes

在不使用作用域的情况下,每个对象的注入都是通过调用 Providers 方法或者 Inject 构造方法获取到的

在使用作用域的情况下,对象的生命周期和所在的 Component 生命周期相同;

作用域首先要指明在哪个 Component 上,然后才能在其 module 上使用。

  • ActivityRetainedScoped
  • ActivityScoped
  • FragmentScoped
  • ServiceScoped
  • ViewModelScoped
  • ViewScoped
  • Singleton

作用域的实现原理

Component 会有一个 xxxProvider 提供作用域注解的对象,这个 xxxProvider 是在 Component 是在构造方法中进行初始化的,其类型为 DoubleCheck ,DoubleCheck作用是,同一个 DoubleCheck 获取的其包裹对象 T 是唯一的。因此同一个 Component 获取作用域注解的对象,是同一个。

Module InstallIn Provides

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(
            appContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }

    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
        return database.logDao()
    }
}

Binds

设置接口和实现的映射关系

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
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {

    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}



class AppNavigatorImpl @Inject constructor(
    private val activity: FragmentActivity
) : AppNavigator {
    ...
}



@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var navigator: AppNavigator

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

        if (savedInstanceState == null) {
            navigator.navigateTo(Screens.BUTTONS)
        }
    }

    ...
}

Qualifier 限定符 和 Named

如果一个需要注入的对象,但是这个注入对象有两个创建方式都可以使用,为了明确指定用哪种创建方式,可以使用 Qualifier 声明两个注解分别添加到创建方式上,用来区分两个创建方式

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
package com.example.android.hilt.di

@Qualifier
annotation class InMemoryLogger

@Qualifier
annotation class DatabaseLogger

@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {

    @DatabaseLogger
    @Singleton
    @Binds
    abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}

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

    @InMemoryLogger
    @ActivityScoped
    @Binds
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}

ActivityContext 和 ApplicationContext

预定义的限定符 (Qualifier) 其具体作用可参考 Qualifier

1
2
3
4
@Qualifier
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ActivityContext {}

EntryPoint

有时候对象的使用并不在已经定义好的切入点中,

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
class LogsContentProvider: ContentProvider() {

    @InstallIn(SingletonComponent::class)
    @EntryPoint
    interface LogsContentProviderEntryPoint {
        fun logDao(): LogDao
    }

    ...
}


class LogsContentProvider: ContentProvider() {
    ...

    private fun getLogDao(appContext: Context): LogDao {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            appContext,
            LogsContentProviderEntryPoint::class.java
        )
        return hiltEntryPoint.logDao()
    }
}

其他

  • HiltViewModel
  • AndroidEntryPoint
  • HiltAndroidApp

错误

1
2
3
  Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? (dagger.hilt.android.plugin)
  See https://dagger.dev/hilt/gradle-setup.html
  [Hilt] Processing did not complete. See error above for details.

没有添加

1
2
3
4
5
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// 所有模块都要添加下面的 plugin  和 引用和 compiler
plugins {
    id 'dagger.hilt.android.plugin'
}

将 arguments = 改为 arguments +=

1
2
3
4
5
6
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                                     "$projectDir/schemas".toString()]
            }
        }

hilt 2.38.1 版本 和 kotlin 1.5.20 版本不兼容。将 kotlin 升级为 1.5.21 就可以解决