Hilt 是Google 最新的依赖注入框架,其是基于Dagger。对于Android开发者来说,Hilt可以说专门为Android 打造,提供了一种将Dagger依赖项注入到Android应用程序的标准方法,而且创建了一组标准的组件和作用域,这些组件会自动集成到Android应用程序的各个生命周期中,以简化开发者的上手难度。
在学习本文之前,假定大家已经了解依赖注入是什么,如果没有了解过,可以先了解概念。Hilt 的目的是降低Android 开发者使用依赖注入框架的上手成本,但是基本的理念大家还是要明白。
@HiltAndroidApp
触发Hilt的代码生成,包括适用于应用程序的基类,可以使用依赖注入,应用程序容器是应用程序的父容器,这意味着其他容器可以访问其提供的依赖项。
@AndroidEntryPoint
其会创建一个依赖容器,该容器遵循Android类的生命周期
@Inject
用来注入的字段,其类型不能为Private
如果要告诉 Hilt 如何提供相应类型的实例,需要将 @Inject 添加到要注入的类的构造函数中。
Hilt有关如何提供不同类型的实例的信息也称为绑定**。**
@Install(xx)
Install 用来告诉 Hilt 这个模块会被安装到哪个组件上.
Hitl默认有以下标准组件,只需要在类上增加
@AndroidEntryPoint
即可支持以下类的注入
Compenent | Injector for |
---|---|
ApplicationComponent |
Application |
ActivityRetainedComponent |
ViewModel (请参阅JetPack-ViewModel扩展) |
ActivityComponent |
Activity |
FragmentComponent |
Fragment |
ViewComponent |
View |
ViewWithFragmentComponent |
View 与 @WithFragmentBindings |
ServiceComponent |
Service |
需要注意的是,Hilt仅支持扩展FragmentActivity
(如AppCompatActivity
)的活动和扩展Jetpack库的片段Fragment
,而不支持Fragment
Android平台(现已弃用)的 片段 。
Component | 作用范围 | Created at | Destroyed at |
---|---|---|---|
ApplicationComponent |
@Singleton |
Application#onCreate() |
Application#onDestroy() |
ActivityRetainedComponent |
@ActivityRetainedScope |
Activity#onCreate() 1 |
Activity#onDestroy() 1 |
ActivityComponent |
@ActivityScoped |
Activity#onCreate() |
Activity#onDestroy() |
FragmentComponent |
@FragmentScoped |
Fragment#onAttach() |
Fragment#onDestroy() |
ViewComponent |
@ViewScoped |
View#super() |
View destroyed |
ViewWithFragmentComponent |
@ViewScoped |
View#super() |
View destroyed |
ServiceComponent |
@ServiceScoped |
Service#onCreate() |
Service#onDestroy() |
默认情况下,所有的绑定都是无作用域,也就是说,每次绑定时,都会创建一个新的绑定实例;
但是,Dagger 允许绑定作用域到特定组件,如上表所示,在指定组件范围内,实例都只会创建一次,并且对该绑定的所有请求都将共享同一实例。
例如:
@Singletion
class TestCompenent @Inject constructor()
其中@Singleton 就代表 TestComponent 实例在整个app中是唯一的,当后续某个类想要注入其时,将共享这个实例。
我们有一个 NetDataSource的 远程数据类,然后我们可能需要在Activity中调用,代码如下
class NetDataSource{ fun test(){ println("我只是一个测试方法") } }
class MainActivity : AppCompatActivity() { lateinit var netDataSource: NetDataSource override fun onCreate(savedInstanceState: Bundle?) { ... netDataSource = NetDataSource() } }
这样用没有什么问题,我们大多数时候都是这样干的,当然在kt中也可以使用 by lazy,不过具体看你自己的场景了。但如何将上面的代码用Hilt 改造呢?
改造后
class NetDataSource @Inject constructor(){ fun test(){ println("我只是一个测试方法") } }
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var netDataSource: NetDataSource override fun onCreate(savedInstanceState: Bundle?) { ... netDataSource.test() } }
这样就结束了吗,如果这样使用,那么就会直接报错,因为Hilt在代码生成时需要访问所有模块,所以必须使用 @HiltAndroidApp 标注你的基类Application.类似如下:
@HiltAndroidApp class KtxApplication : Application()
同样,添加了@HiltAndroidApp 之后,你也可以在后续任意位置使用到 @ApplicationContext
模块用于向 Hlit 添加绑定,换句话说,是告诉 Hlit 如何提供不同类型的实例。
增加了@Module注解的类,其代表着相当与一个模块,并通过指定的组件来告诉在哪个容器中可以使用绑定安装。
对于Hilt可以注入的每个Android类,都有一个关联的 Hilt Component,例如,Application 容器与之关联 ApplicationComponent ,并且Fragmenet容器与关联的 FragmentComonent,就是最开始我们讲的组件生命周期
@InstallIn(ApplicationComponent::class) @Module object TestModule { //每次都是新的实例 @Provides fun bindBook():Book{ return Book() } //全局复用同一个实例 @Provides @Singleton fun bindBook():BookSingle{ return Book() } }
Install 用来告诉hilt 这个模块会被安装到哪个组件上。
一个常见的误解是,模块中声明的所有绑定都将作用于安装该模块的组件。但是,事实并非如此。仅使用范围注释注释的绑定声明将被限制范围。
那什么时候添加注入范围呢?
对绑定进行作用域限定会在生成的代码大小和其运行时性能上付出代价,因此请谨慎使用作用域。确定绑定是否应限制作用域的一般规则是,仅在代码正确性需要绑定作用域时才对绑定进行作用域。如果您认为绑定仅出于性能方面的考虑而作用域,请首先验证性能是否存在问题,然后考虑使用
@Reusable
而不是组件作用域。
注意:在Kotlin中,仅包含@Provides函数的模块可以是object类。这样,提供程序就可以得到优化,并且几乎可以内联在生成的代码中。
用来告诉Hilt 如何提供不能被构造函数注入的类型
每当 Hilt 需要提供该类型的实例时,将执行带注释的函数的函数主体。@Provides 常用于模块中
我们使用room,有一个数据库表和相应的Dao
@Entity(tableName = "book") class Book(val name: String) { @PrimaryKey(autoGenerate = true) var id: Long = 0 } @Dao interface BookDao { @Insert suspend fun insertAll(vararg books: Book) @Query("SELECT COUNT(*) FROM book") suspend fun queryBookAll(): Int }
接着,我们还有一个AppDataBase 和 一个单例的 roomDatabase
@Database(entities = [Book::class], version = 1) abstract class AppDataBase : RoomDatabase() { abstract fun bookDao(): BookDao } object RoomSingle { lateinit var context: Context val roomDatabase by lazy { Room.databaseBuilder( context, AppDataBase::class.java, "logging.db" ).build() } }
一般我们在使用时,都采用如下写法:
class MainActivity : AppCompatActivity() { val bookDao by lazy { RoomSingle.roomDatabase.bookDao() } }
我们创建一个BookModule,并使用 @Model 注明这是一个模块,@InstallIn 声明这个模块的生命范围为APP级别
@InstallIn(ApplicationComponent::class) @Module object BookModule { @Provides @Singleton fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase { return Room.databaseBuilder( appContext, AppDatabase::class.java, "book.db" ).build() } @Provides fun provideBookDao(database: AppDatabase): BookDao { return database.bookDao() } }
我们为其中的 provideBookDao 增加了@Provides,其意义是告诉Hilt 提供实例BookDao
时,需要执行database.bookDao()
。由于我们具有AppDatabase
传递依赖关系,因此我们还需要告诉Hilt如何提供该类型的实例。
由于AppDatabase
是由Room生成的,因此是项目不拥有的另一个类,因此我们直接复制原方法即可,这里的 @Singleton 标志这个其方法只会被调用一次,类似于一个单例。
更改 MainActivity 中的代码如下:
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var bookDao: BookDao }
到现在为止,我们就算改造完成了,这样的话,我们在任意位置都可以获得这个BookDao.而且免除手动构造
对于接口,无法使用构造函数进行注入,我们需要告诉Hilt使用哪种实现。Binds的作用就在于此。
需要注意以下使用条件:
我们有一个 IBook 接口,用来存储及查询书本数据
interface IBook { suspend fun saveBook(name: String) suspend fun getBookAllSum(): Int }
接着如果我们想在别的地方拿到这个接口对象,常规的实现方式可能就是 你的某个具体实现类实现了其,然后在需要使用的地方 再 val iBook=xxxImpl()
如果用Hint呢?,继续代码演示
接着有一个具体的实现类 BookImpl ,这里我们使用构造函数注入 并且注入了 BookDao 用来处理具体的数据存储。这里我们用到了挂起函数,对于这块不怎么熟系的同学,可以理解为,其相当于一个标记位,提示编译器这块可能会有耗时操作,挂起函数即逻辑上的一个处理。具体理解可参考扔物线等大佬的解释,这里不做过多解释。
class BookImpl @Inject constructor() : IBook { @Inject lateinit var dao: BookDao override suspend fun saveBook(name: String) { //这里可能会有一些自己的处理... dao.insertAll(Book(name)) } override suspend fun getBookAllSum(): Int { return dao.queryBookAll() } }
然后我们需要一个新的模块,用来实现 IBook 接口的注入
@InstallIn(ApplicationComponent::class) @Module abstract class BookModel { @Binds abstract fun getIBook(impl: BookImpl): IBook }
在某个Activity使用时(Demo示例,实际开发我们更推荐数据处理放在Repository中,由ViewModel管理):
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var iBook: IBook override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btnSave.setOnClickListener { lifecycleScope.launch(Dispatchers.IO) { iBook.saveBook("Android艺术") val sum = iBook.getBookAllSum() Log.e("petterp", "当前长度-----$sum") } } } }
我们在上面的例子中,只有一个具体实现类,但是往往实际开发,我们是存在多个具体实现。而且他们的作用域也都不同,有些可能只是某个Activity使用,有些是全局使用,对于这种问题我们如何解决呢?
我们可以为两个具体实现定义不同的模块并使用Qualifers规定。
依然以上面的 代码延续。此时有另一个实现,想实现有特殊条件的存储。
另一个实现类为:BookConditionImpl
class BookConditionImpl @Inject constructor() : IBook { @Inject lateinit var dao: BookDao override suspend fun saveBook(name: String) { if (name == "Android艺术") { dao.insertAll(Book("Petterp")) } } override suspend fun getBookAllSum(): Int { return dao.queryBookAll() } }
然后我们更改 BookModel,新加入BookConditionModel 模块
@InstallIn(ApplicationComponent::class) @Module abstract class BookModel { @Binds @Singleton abstract fun getIBook(impl: BookImpl): IBook } @InstallIn(ActivityComponent::class) @Module abstract class BookConditionModel { @Binds @ActivityScoped abstract fun getConditionIBook(impl: BookConditionImpl): IBook }
接着添加两个注解,其中使用到了 @Qualifier
@Qualifier annotation class BookModelSingle @Qualifier annotation class BookModelCondition @InstallIn(ApplicationComponent::class) @Module abstract class BookModel { @BookModelSingle @Binds @Singleton abstract fun getIBook(impl: BookImpl): IBook } //其模块范围为Activity @InstallIn(ActivityComponent::class) @Module abstract class BookConditionModel { @Binds @ActivityScoped @BookModelCondition abstract fun getConditionIBook(impl: BookConditionImpl): IBook }
然后更改我们的Activity
@AndroidEntryPoint class MainActivity : AppCompatActivity() { @BookModelCondition @Inject lateinit var iBookCondition: IBook @BookModelSingle @Inject lateinit var iBook: IBook }
作为Google推荐的依赖注入组件,目前Hilt 可以与ViewModel配合使用
导入依赖
allprojects {
repositories {
google()
jcenter()
maven {
url "https://androidx.dev/snapshots/builds/6543454/artifacts/repository/"
}
}
}
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
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'
我们创建一个MainActivty
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<TestViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.test()
}
}
ViewModel
class TestViewModel @ViewModelInject constructor(
private val repository: TestRepository,
@Assisted val savedState: SavedStateHandle
) : ViewModel() {
fun test() {
repository.test()
}
}
TestRepository
@ActivityScoped
class TestRepository @Inject constructor() {
fun test() {
Log.e("petterp", "一个测试方法")
}
}
到目前为止,Hilt相关内容基本就结束了,目前对于Hilt的资料较少,如果想要用到项目中,建议多试多看文档,再做决定。
参考资料,按照优先级:
new JetPack
Hilt-代码实验室
DaggerDev
Google-iosched