Hilt以Android专属DI框架的身份继续完善了Jetpack的布局。它在前辈Dagger2的基础上做了诸多改善,同时又存在很多限制,本文将逐一回答。
先来看下官方对于Hilt的描述。
Hilt provides a standard way to incorporate Dagger dependency injection into an Android application.
To simplify Dagger-related infrastructure for Android apps.
To create a standard set of components and scopes to ease setup, readability/understanding, and code sharing between apps.
To provide an easy way to provision different bindings to various build types (e.g. testing, debug, or release).
正如描述的那样,Hilt是在Dagger(Dagger2)的基础上专为Android App打造的依赖注入方案。它在保留Dagger2的编译时注入的性能优势前提下,简化了注解的使用。同时针对Android框架类进行了优化。
在展开Hilt的讲述之前先来简单回顾下依赖注入的各个角色和流程。
给Application添加@HiltAndroidApp注解即可告知Hilt生成应用级别的组件,自动实现了依赖注入的起点,免去了Dagger2的手动调用。
@HiltAndroidApp
class MyApplication : Application() {
...}
@AndroidEntryPoint注解用来为Activity,Fragment,Service等Android框架类生成Hilt组件,省去了定义相应SubComponent的模板处理。
@AndroidEntryPoint
open class BaseActivity() : AppCompatActivity() {
...}
@InstallIn注解可以告知Hilt每个模块将用在或绑定到哪个Android类中。比如指定的value为ApplicationComponent的话将表明该模块在整个应用周期内只会实例化一份,即单例。其他的还有绑定到Activity生命周期的ActivityComponent。
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
...}
@Singleton和@ActivityRetainedScoped等注解用以声明该注入的作用范围。比如Activity因为Configuration Change重绘了但@ActivityRetainedScoped注释的依赖的并不会重新创建。
@ActivityRetainedScoped
class MovieAdapter @Inject constructor() {
... }
@AndroidEntryPoint
class DemoActivity : AppCompatActivity() {
@Inject lateinit var movieAdapter: MovieAdapter
...
}
通过@ApplicationContext 和@ActivityContext注解等可以快速注入Context实例,省得我们自己提供Context的实现。
class MovieAdapter @Inject constructor(@ActivityContext private val context: Context)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
...}
Hilt实现了一些扩展帮助我们注入ViewModel和WorkManager的依赖。比如@ViewModelInject注解就可以告知Hilt此处需要注入ViewModel实例。
class MovieViewModel @ViewModelInject constructor(private val repository: Repository,
var movieAdapter: MovieAdapter
) : ViewModel() {
...}
Hilt和Dagger2一样支持@Qualifier定义多类型注入的注解。@Inject,@Provides以及@Binds的使用也没有差别,不再赘述。感兴趣的可以查询官方文档获得更详尽的介绍。
https://developer.android.google.cn/training/dependency-injection/hilt-android
照例使用OMDB API演示下Hilt的使用。
总体上通过@ViewModelInject向ViewModel注入Repository,Repository依赖RemoteData和LocalData。
Hilt Demo
Hilt的改善必然伴随着一些限制,遵照了这些限制Hilt才能发挥它的优势。
1. 框架类存在依附类的话同样需要添加@AndroidEntryPoint
假使只给Framgent添加了@AndroidEntryPoint但所属Activity没有添加的话,启动Fragment的时候会发生如下异常。
Hilt Fragments must be attached to an @AndroidEntryPoint Activity.
原理在于Fragment在attach的时候后会确保Fragment依附的Activity实现了GeneratedComponentManager接口,即是否添加了@AndroidEntryPoint注解。
2. 仅支持扩展自ComponentActivity的Activity
如果@AndroidEntryPoint注释的Activity并非ComponentActivity的子类,那么在编译阶段就无法通过。
Activities annotated with @AndroidEntryPoint must be a subclass of androidx.activity.ComponentActivity.
@AndroidEntryPoint注释的Activity是支持ViewModel注入的,而ViewModel的实现完全依赖于ComponentActivity,所以作此限制很有必要。毕竟都已经用Jetpack全家桶了,Activity这么重要的组件还用老的也太没决心了。
3. 仅支持扩展自androidx.Fragment包的Fragment
和Activity的限制一样,采用AOSP的Fragment的话,编译阶段就不会让你通过。
@AndroidEntryPoint base class must extend ComponentActivity, (support) Fragment, View, Service, or BroadcastReceiver.
事实上AOSP的Fragment自Android 9 就已Deprecated,在于其缺乏很多特性,包括Lifecycle,ViewMode等。进而无法和ComponentActivity搭配使用。
4. 也不支持Retained Fragment
调用setRetainInstance(true)的Fragment就是Retained Fragment。意味着Configuration Change导致持有的view销毁了但Fragment本身没有销毁。
如果不小心将Retained Fragment添加了@AndroidEntryPoint,那么在横竖屏切换导致画面重绘的时候可能会发生如下异常。
onAttach called multiple times with different Context! Hilt Fragments should not be retained.
Configuration Change导致Activity重建,但Retained Fragment实例保留了下来。意味着同一个Fragment实例要被重复注入依赖,这并不合理。所以Hilt在第一次注入Fragment前会依据依附的Activity创建一个Context实例,后续将检查这个实例是否为空来确保每次注入的Fragment都是新的。
5. 向Activity等框架类注入实例的话需要采用字段注入
Application、Activity等Android特有类的实例由系统创建,无法通过构造函数注入,只能采用字段注入的方式。
6. 注入的字段不能为私有的
不小心将注入的字段声明成private也没关系,编译期会向你发出提醒。
Execution failed for task ‘:app:kaptDebugKotlin’. A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution.
7. 框架类的基类可统一添加@AndroidEntryPoint,但抽象类则不需要
框架类的基类一次性添加了这个注解各Hilt便可生成统一的组件向各子类注入依赖,每个子类不用额外添加。但基类是抽象类的话则不可以使用该注解,每个子类仍然需要各自添加。否则会发生编译错误。
8 无法为BrocastReceiver生成独立的Component组件
不同于Activity和Fragment,Hilt直接从ApplicationComponent组件向BrocastReceiver注入依赖。从原理上讲BrocastReceiver实例的创建和回调都是由ApplicationThread直接调度的,所以设计成这样?
9. ActivityRetainedComponent组件在第一次调用onCreate()时创建,在最后一次调用Activity#onDestroy()时销毁。
ActivityRetainedComponent组件在Configuration Change导致Activity重绘后仍然存在,生命周期长于ActivityComponent组件。可以添加@ActivityRetainedScope注解来绑定这个组件。
需要提醒的一点是如果采用@ViewModelInject提供ViewModel的依赖,那么无需再使用@ActivityRetainedScope来注释依赖的实例,因为它已经被自动绑定到了ActivityRetainedComponent组件。
10. 默认的绑定在每次注入都会创建新的实例。
基于内存开销的考虑,默认情况下都绑定都没有限定作用域,即注入每个依赖的地方都会提供一个新的实例。如果被依赖的实例具有明确的使用场景或范围,可以为这个注入指定作用域,比如整个应用周期内保留一份实例的@Singleton作用域。
11. View里进行的字段注入默认将绑定到ActivityComponent组件
如果View来自于Fragment,在@AndroidEntryPoint以外可以添加@WithFragmentBindings将注入精准地绑定到FragmentComponent组件。
12. ContentProvider不能直接使用Hilt注解
四大组件中只有ContentProvider实例的创建先于Application,可是整个应用的注入起点是Application组件,所以无法直接为ContentProvider提供注入的支持。
但如果ContentProvider确有注入需求的话,需要自行定义@EntryPoint注释的接口并通过@InstallIn指定依赖项需要绑定到的组件,具体的使用过程和跟Dagger2很类似。
13. 使用@ViewModelInject向ViewModel注入依赖的特别注意
Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper ‘-jvm-target’ option
相较于Dagger2的改善足以窥见Hilt的诸多优点。
但Hilt也不是完美的,除了上述罗列的一堆限制以外,还存在天生的劣势,比如无法应用在动态功能模块项目当中。依据实际需要做出抉择,是小而美的Hilt,还是大而强的Dagger2。
Hilt Demo
https://developer.android.google.cn/training/dependency-injection/hilt-android
https://developer.android.google.cn/training/dependency-injection/hilt-jetpack
https://mp.weixin.qq.com/s/VKyyNqAPFnlclGKnIbisAw
https://guolin.blog.csdn.net/article/details/109787732
Dagger2和它在SystemUI上的应用
除了SQLite一定要试试Room