Dagger Hilt
Android端有不少DI框架可供选择 -- 例如用于控件注入的ButterKnife
、用于Kotlin的Koin
等,但唯有Dagger才称得上是谷歌官方认可的DI方案。
Dagger最早由Square开发,后被谷歌fork并升级为Dagger2,成为了Android官方推荐的DI最佳实践。Dagger较好地实现了JSR-330
规范,虽然功能强大,但是无法很好地应对Android项目。谷歌随后推出dagger-android
(及dagger-android-support
),试图通过新的注解降低Android开发中Dagger的使用成本,但效果并不理想,因此 Android Dev Summit 2019
上Dagger Hilt发布了,并在今年正式推出了alpha版
https://developer.android.com/training/dependency-injection/hilt-android
有些文章说Hilt是替代Dagger的,更准确的说法是用来替代dagger-android
的。Hilt的名字非常秒,其目的就是帮助初学者在Android开发中更好地使用Dagger,代理了很多复杂的初始配置,大大降低开发成本,避免了来自“匕首”的反噬。
接下来通过一个简单的例子,学习一下Dagger Hilt的基本使用。
https://github.com/vitaviva/DaggerHiltSample
<br/>
Gradle
// build.gradle
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
// app/build.gradle
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:hilt-android:2.28-alpha'
kapt 'com.google.dagger:hilt-android-compiler:2.28-alpha'
}
<br/>
Application
使用dagger-andorid
时,需要定义App级别的Component
同时声明其依赖的Module
@Singleton
@Component(modules = [
AndroidInjectionModule::class,
ActivityModule::class,
FragmentModule::class,
ViewModelModule::class])
interface AppComponent : AndroidInjector<DaggerApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance application: Application): AppComponent
}
}
然后要么继承DaggerApplication
要么实现HasAndroidInjector
接口,来创建Component。
class MyApplication : DaggerApplication() {
override fun applicationInjector() = DaggerAppComponent.factory().create(this)
}
现在使用Hilt,只需@HiltAndroidApp
一个注解,搞定上面这一切。
@HiltAndroidApp
class App : Application() {
}
<br/>
Component & Module
dagger-andorid
需要向上面那样通过@Component
定义AppComponent,那么Hilt是如何创建AppComponent,又是如何确定其依赖的Module的呢?。
Hilt已经为各种Android组件预置了Component
Hilt的Module也不需要在Component中声明,而是使用@InstallIn
在定义Modle时反向声明Component
// ApplicationModule.kt
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Singleton
@Provides
fun provide(): String {
return hashCode().toString()
}
}
//ActivityModule.kt
@Module
@InstallIn(ActivityComponent::class)
class ActivityModule {
@ActivityScope
@Provides
fun provide(): String {
return hashCode().toString()
}
}
例子中Provide的类型都是String
,所以自定义注解加以区分,@Qualifier
的使用与以往没有区别
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class AppScope
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ActivityScope
<br/>
Activity & Fragment
上面简简单单就完成了DI的Provide侧实现,接下来看Inject侧 -- 主要是针对Activity以及Fragment进行注入。dagger-android
通过@ContributesAndroidInjector
帮我们生成SubComponent
;通过继承DaggerAppCompatActivity
可以在onCreate时对Activity进行自动注入。虽然节省了一些模板代码,但是@ContributesAndroidInjector的出现某种程度上又成了新的模板代码。
@Module
abstract class ActivityModule {
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentModule::class])
internal abstract fun contributeMainActivity(): MainActivity
@ActivityScope
@ContributesAndroidInjector
internal abstract fun contributeSecondActivity(): SecondActivity
}
由于Hilt有了各预置Component,不再依赖SubComponent
的创建(准确地说是无需开发者自定义SubComponent了,但是仍然会根据预置Component创建SubComponent,例如针对ActivityComponent生成Hilt_ActivityComponent
,再次印证了Hilt只用来替代dagger-android,底层仍然依靠Dagger的运作机制),只需要@AndroidEntryPoint
一个注解实现Activity等组件的注入。
@AndroidEntryPoint
通过字节码插桩在编译期改变目标类对象的继承结构(例如MainActivity与AppComponentActivity之间插入Hilt_MainActivity
作为父类),在父类的的各生命周期回调中,创建上述SubComponent并对目标实时注入
Component | Description | Created at | Destroyed at |
---|---|---|---|
ApplicationComponent | 为App提供依赖 | Application#onCreate() | Application#onDestroy() |
ActivityComponent | 为Activity提供依赖 | Activity#onCreate() | Activity#onDestroy() |
ActivityRetainedComponent | Retained顾名思义,其生命周期更长,不会因屏幕旋转等因素重建,实际上是借助ViewModel实现的 | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | 为Fragment提供依赖 | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | 为View提供依赖,构造函数中进行注入 | View#super() | View destroyed |
ViewWithFragmentComponent | 为Fragment中的View提供依赖 | View#super() | View destroyed |
ServiceComponent | 为Service提供依赖 | Service#onCreate() | Service#onDestroy() |
像SubComponent一样,预置Component的Scope同样具有继承关系
// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.activity_main) {
@AppScope
@Inject
lateinit var appHash: String
@ActivityScope
@Inject
lateinit var activityHash: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.v(TAG, "app : $appHash")
Log.v(TAG, "activity : $activityHash")
}
}
// MainFirstFragment.kt
@AndroidEntryPoint
class FirstFragment : Fragment(R.layout.fragment_first) {
@AppScope
@Inject
lateinit var appHash: String
@ActivityScope
@Inject
lateinit var activityHash: String
@FragmentScope
@Inject
lateinit var fragmentHash: String
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "app : $appHash")
Log.d(TAG, "activity : $activityHash")
Log.d(TAG, "fragment : $fragmentHash")
}
}
<br/>
ViewModel
随着MVVM架构的推广,ViewModel成为了Android项目的标配。但是ViewModel的注入一向是比较繁琐的。一方面ViewModel大都是ViewModelFactory
提供的,我们无法自定义Provider,另一方面Factory大都是反射创建ViewModel,所以无法进行构造器注入,ViewModel最近新添加的SavedStateHandle
就比较难处理。
目前为止,常用@IntoMap
配合ViewModelFactory的定义实现ViewModel的注入。
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(ActivityViewModel::class)
abstract fun bindActivityViewModel(viewModel: ActivityViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(FragmentViewModel::class)
abstract fun bindFragmentViewModel(viewModel: FragmentViewModel): ViewModel
}
@Singleton
class ViewModelFactory @Inject constructor(
private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val found = creators.entries.find { modelClass.isAssignableFrom(it.key) }
val creator = found?.value
?: throw IllegalArgumentException("unknown model class " + modelClass)
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Hilt提供了androidx的扩展库,很好地解决了ViewModel的注入问题
关于ViewModel注入的更多细节,可以参考我的另一篇文章
ViewModel的依赖注入及实现原理
Gradle
首先,配置androidx仓库地址
// build.gradle
allprojects {
repositories {
maven {
url "https://androidx.dev/snapshots/builds/6543454/artifacts/repository/"
}
}
}
// app/build.gradle
dependencies {
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
implementation 'androidx.hilt:hilt-common:1.0.0-SNAPSHOT'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-SNAPSHOT'
kapt 'androidx.hilt:hilt-compiler:1.0.0-SNAPSHOT'
}
ViewModel
通过@ViewModelInject
进行构造器注入,无需构造任何Factory。SavedStateHandle
使用@Assisted
注解
class ActivityViewModel @ViewModelInject constructor(
private val repository: Repository,
@Assisted private val savedState: SavedStateHandle
) : ViewModel() {
val repository(): String = repository.toString()
}
@Singleton
mock一个远程Repo,这个目前为止的写法一样
@Singleton
class Repository @Inject constructor() {
fun getSomething(): Something
}
Activity & Fragment
Activity和Fragment像往常一样通过ktx的viewModels
、activityViewModels
获取ViewModel即可
// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel by viewModels<ActivityViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.v(TAG, "repository : $repository")
Log.v(TAG, "activity vm : $viewModel")
Log.v(TAG, "activity vm repo : ${viewModel.repository}")
}
}
FirstFragment.kt
@AndroidEntryPoint
class FirstFragment : Fragment(R.layout.fragment_first) {
private val activityViewModel by activityViewModels<ActivityViewModel>()
private val fragmentViewModel by viewModels<FragmentViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG, "activity vm: $activityViewModel")
Log.d(TAG, "fragment vm: $fragmentViewModel")
Log.d(TAG, "activity vm repo: ${activityViewModel.repository}")
Log.d(TAG, "fragment vm repo: ${fragmentViewModel.repository}")
}
}
Repository由于是@Singleton的,所以全局单例存在。ViewModel则根据by
位置的不同以多实例存在
<br/>
Summary
通过例子可以感受到,Hilt
相对于dagger-android
减少了大量的模板代码,但也以为着灵活性上的降低,例如若使用预置ActivityComponent
,则针对所有Activity都可以提供同样的注入,代码隔离上达不到自定义Component那样的精细化程度。但是相对于开发体验的提升,这点牺牲不算什么。Hilt作为官方推荐的DI库,未来的前景十分值得期待~
Sample repo:
https://github.com/vitaviva/DaggerHiltSample