查看原文
其他

Jetpack Hilt有哪些改善又有哪些限制?

小虾米君 TechMerger 2021-10-12
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中

地址

https://github.com/ellisonchan/JetpackDemo

框图 & 截图


Hilt的限制

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注入依赖的特别注意
  • 如果使用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://github.com/ellisonchan/JetpackDemo

参考资料

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

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存