Hilt
以Android专属DI框架的身份继续完善了Jetpack的布局。它在前辈Dagger2
的基础上做了诸多改善,同时又存在很多限制,本文将逐一回答。
Hilt的由来
先来看下官方对于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的讲述之前先来简单回顾下依赖注入的各个角色和流程。
依赖注入流程
- 依赖的需求方,通过构造参数或字段依赖其他实例的角色,一般使用
@Inject
描述这种需求 - 依赖的提供方,对被依赖的实例提供实现的角色,比如使用
@Provides
描述这种来源 - 依赖的注入方,将提供方的实现注入到需求方的角色,比如使用
@Component
描述这种注入组件
Hilt的改善
定义应用组件
给Application添加@HiltAndroidApp
注解即可告知Hilt生成应用级别的组件,自动实现了依赖注入的起点,免去了Dagger2的手动调用。
@HiltAndroidApp class MyApplication : Application() {...}
定义Android框架类组件
@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 ... }
注入Context
通过@ApplicationContext
和@ActivityContext
注解等可以快速注入Context实例,省得我们自己提供Context的实现。
class MovieAdapter @Inject constructor(@ActivityContext private val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {...}
Jetpack组件的支持
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
实战DEMO
照例使用OMDB API演示下Hilt的使用。
总体上通过@ViewModelInject向ViewModel注入Repository,Repository依赖RemoteData和LocalData。
RemoteData通过NetworkModule提供单例的Retrofit接口,向OMDB发出搜索电影的请求
LocalData依赖AnalysisModule提供的AnalysisService接口将选中的电影记录进RoomModule提供的Room Database中
地址
框图 & 截图
Hilt的限制
Hilt的改善必然伴随着一些限制,遵照了这些限制Hilt才能发挥它的优势。
@AndroidEntryPoint 的限制
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. 框架类的基类可统一添加@AndroidEntryPoint,但抽象类则不需要
框架类的基类一次性添加了这个注解各Hilt便可生成统一的组件向各子类注入依赖,每个子类不用额外添加。但基类是抽象类的话则不可以使用该注解,每个子类仍然需要各自添加。否则会发生编译错误。
Android框架类的注入限制
Android框架类的注入限制 | 限制内容 |
注入方式 | 只能是字段注入 |
字段的修饰符 | 不能为private |
BrocastReceiver | 不会生成单独的组件 |
View | 默认绑定到ActivityComponent |
ContentProvider | 无法直接使用@AndroidEntryPoint注解 |
1. 向Activity等框架类注入实例的话需要采用字段注入
Application、Activity等Android特有类的实例由系统创建,无法通过构造函数注入,只能采用字段注入的方式。
2. 注入的字段不能为私有的
不小心将注入的字段声明成private也没关系,编译期会向你发出提醒。
Execution failed for task ‘:app:kaptDebugKotlin’. A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution.
3 无法为BrocastReceiver生成独立的Component组件
不同于Activity和Fragment,Hilt直接从ApplicationComponent组件向BrocastReceiver注入依赖。从原理上讲BrocastReceiver实例的创建和回调都是由ApplicationThread直接调度的,所以设计成这样?
4. View里进行的字段注入默认将绑定到ActivityComponent组件
如果View来自于Fragment,在@AndroidEntryPoint以外可以添加@WithFragmentBindings将注入精准地绑定到FragmentComponent组件。
5. ContentProvider不能直接使用Hilt注解
四大组件中只有ContentProvider实例的创建先于Application,可是整个应用的注入起点是Application组件,所以无法直接为ContentProvider提供注入的支持。
但如果ContentProvider确有注入需求的话,需要自行定义@EntryPoint注释的接口并通过@InstallIn指定依赖项需要绑定到的组件,具体的使用过程和跟Dagger2很类似。
生命周期的注意
生命周期的注意 | 限制内容 |
默认的绑定 | 每次注入都会创建新的实例 |
ActivityRetainedComponent | 最后一次destroy时销毁 |
1. 默认的绑定在每次注入都会创建新的实例。
基于内存开销的考虑,默认情况下都绑定都没有限定作用域,即注入每个依赖的地方都会提供一个新的实例。如果被依赖的实例具有明确的使用场景或范围,可以为这个注入指定作用域,比如整个应用周期内保留一份实例的@Singleton作用域。
2. ActivityRetainedComponent组件在第一次调用onCreate()时创建,在最后一次调用Activity#onDestroy()时销毁。
ActivityRetainedComponent组件在Configuration Change导致Activity重绘后仍然存在,生命周期长于ActivityComponent组件。可以添加@ActivityRetainedScope注解来绑定这个组件。
需要提醒的一点是如果采用@ViewModelInject提供ViewModel的依赖,那么无需再使用@ActivityRetainedScope来注释依赖的实例,因为它已经被自动绑定到了ActivityRetainedComponent组件。
ViewModel注入的注意点
使用@ViewModelInject向ViewModel注入依赖需要留意如下的特别注意。
如果使用viewModels()的KTX获取ViewModel实例的话,可能会遇到找不到该KTX的问题,这时候注意下gradle文件有没有导入fragment-ktx的依赖
如果运行失败并提示viewmodel不包含默认构造函数,一定记得检查下hilt-compiler的注释处理器有没有在gradle文件里声明
如果编译发生如下的错误,需记得在gradle的kotlinOptions里声明jvm的版本为1.8
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的诸多优点。
高度封装了框架类的注入免于手动初始化组件
预设了充分的作用域和方便的Context注入
针对Jetpack组件的支持
当我们需要在Android App上导入DI的话可以优先考虑它。
但Hilt也不是完美的,除了上述罗列的一堆限制以外,还存在天生的劣势,比如无法应用在动态功能模块项目当中。依据实际需要做出抉择,是小而美的Hilt,还是大而强的Dagger2。
本文DMEO
参考资料
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