谷歌接管Dagger后,推出了自己的Hilt框架,Hilt基于Dagger做了一层封装,大大简化了Dagger的使用,定制了一系列规范,并支持Jetpack中部分组件,是一个专门为安卓开发的DI框架
一、构造函数注入
和Dagger相同,Hilt也分两种注入方式,以上篇Dagger中的代码为例子,来对比两个框架的使用区别
1.gradle中配置依赖
工程gradle中导入插件:
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.4"
}
moudle中进行依赖:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
...
def hilt_version = "2.40.4"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
2.使用@Inject注解定义需要注入的类
/**
* 模拟本地数据源
*/
class LocalDataSource @Inject constructor()
/**
* 模拟远程数据源
*/
class RemoteDataSource @Inject constructor()
定义包装类DataSource,包含上面两个类,同样对构造函数使用@Inject注解
/**
* 数据源包装类
* Created by aruba on 2021/12/4.
*/
data class DataSource @Inject constructor(
var remoteDataSource: RemoteDataSource,
var localDataSource: LocalDataSource
)
3.使用@HiltAndroidApp注解Application,表示注入中间件
Dagger使用的是@Component注解表示一个组件,上篇文章也提到过,一个项目对应一个Component就足够了,Hilt规范了Component
@HiltAndroidApp
class App : Application()
4.在Activity中使用@Inject注解对象
class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataSource: DataSource
...
5.对Activity使用@AndroidEntryPoint注解
在Dagger中需要调用Component的注入方法,Hilt中直接使用注解就可以实现注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataSource: DataSource
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("aruba_log", dataSource.toString())
}
}
日志结果:
I/aruba_log: DataSource(remoteDataSource=com.aruba.hiltapplication.di.datasource.RemoteDataSource@f7f11fd, localDataSource=com.aruba.hiltapplication.di.datasource.LocalDataSource@743eef2)
和Dagger相比,我们多导入了一个插件,此插件是利用Javassist,将编译后将@AndroidEntryPoint注解的Activity继承至自己生成的类
下面是该例子生成的Hilt_MainActivity:
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
private volatile ActivityComponentManager componentManager;
private final Object componentManagerLock = new Object();
private boolean injected = false;
Hilt_MainActivity() {
super();
_initHiltInternal();
}
Hilt_MainActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}
// 开始注入
private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
// 注入
inject();
}
});
}
@Override
public final Object generatedComponent() {
return this.componentManager().generatedComponent();
}
protected ActivityComponentManager createComponentManager() {
return new ActivityComponentManager(this);
}
@Override
public final ActivityComponentManager componentManager() {
if (componentManager == null) {
synchronized (componentManagerLock) {
if (componentManager == null) {
componentManager = createComponentManager();
}
}
}
return componentManager;
}
protected void inject() {
if (!injected) {
injected = true;
// 调用component的注入方法
((MainActivity_GeneratedInjector) this.generatedComponent()).injectMainActivity(UnsafeCasts.unsafeCast(this));
}
}
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
}
}
二、模块注入
1.依赖网络框架
dependencies {
...
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
}
别忘了在Manifest.xml中添加权限
2.定义Retrofit API
interface BaiduApiService {
@GET("/index.html")
fun index(): Call
}
3.定义模块
- 和Dagger相同,使用@Moudle注解就可以表示一个模块,使用@Provides注解提供给Component生成注入对象的方法
- 使用@InstallIn注解,指定该模块需要装载到哪些Component中,并且我们不必再定义组件了,Hilt预定义了我们移动开发中所需的组件和子组件
这边指定其装载到SingletonComponent中,也就是全局APP中,旧版本的ApplicationComponent已废弃
@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {
@Provides
fun getBaiduApiService(): BaiduApiService {
return Retrofit.Builder()
.baseUrl("https://www.baidu.com")
.addConverterFactory(ScalarsConverterFactory.create())
.build().create(BaiduApiService::class.java)
}
}
4.在Activity中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataSource: DataSource
@Inject
lateinit var baiduApiService: BaiduApiService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("aruba_log", dataSource.toString())
getIndex()
}
/**
* 获取百度首页
*/
private fun getIndex() {
baiduApiService.index().enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
findViewById(R.id.tv_hello).text = response.body()
}
override fun onFailure(call: Call, t: Throwable) {
}
})
}
}
效果:
三、预定义组件与作用域
1.预定义的Component
Hilt定义的组件为SingletonComponent,子组件在dagger.hilt.android.components包下
这些组件对应的生命周期为:
组件 | 创建时机 | 销毁时机 |
---|---|---|
SingletonComponent | Application#onCreate() | Application#onDestroy() |
ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() |
ServiceComponent | Service#onCreate() | Service#onDestroy() |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel#super() | ViewModel#clear() |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View#super() | 视图销毁时 |
ViewWithFragmentComponent | View#super() | 视图销毁时 |
2.预定义的Scope
Hilt定义的子组件作用域在dagger.hilt.android.scopes包下
这些作用域都是和子组件一一对应的,组件的层级关系如下图:
3.模块中使用作用域
Hilt的作用域就简单很多了,因为它预定义了组件和子组件 ,同时又定义了这些组件对应的作用域,上面的例子中,如何保证只实例化一份?使用SingletonComponent对应的作用域@Singleton即可,使用方法也是和Dagger相同的
@InstallIn(SingletonComponent::class) //表示全局组件
@Module
class NetworkModule {
@Singleton
@Provides
fun getBaiduApiService(): BaiduApiService {
return Retrofit.Builder()
.baseUrl("https://www.baidu.com")
.addConverterFactory(ScalarsConverterFactory.create())
.build().create(BaiduApiService::class.java)
}
}
在Activity中,注入两个对象,并打印下两者的hashcode:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var baiduApiService1: BaiduApiService
@Inject
lateinit var baiduApiService2: BaiduApiService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i("aruba_log","baiduApiService1 hashcode:${baiduApiService2.hashCode()}")
Log.i("aruba_log","baiduApiService2 hashcode:${baiduApiService2.hashCode()}")
}
}
日志结果:
I/aruba_log: baiduApiService1 hashcode:174572891
I/aruba_log: baiduApiService2 hashcode:174572891
4.构造方法使用作用域
ViewModelComponent是新出的子组件,对应的作用域为ViewModelScope,作用为:一个ViewModel中多个同类型注入对象,则使用同一份实例。以前实现ViewModel中注入还需要依赖其他框架,这次来使用ViewModelScope作为例子
4.1 定义注入类,并使用@ViewModelScope注解
注意:如果把参数放入主构造,并且赋了默认值,那么会生成两个构造方法,Hilt就会报错
@ViewModelScoped
class UserInfo @Inject constructor() {
var name: String = "张三"
}
4.2 定义ViewModel,并为其添加@HiltViewModel注解
在构造中注入对象
@HiltViewModel
class MainViewModel @Inject constructor(var userInfo: UserInfo) : ViewModel()
4.3 Activity中使用
@AndroidEntryPoint
class ViewModelActivity : AppCompatActivity() {
val viewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)
Log.i("aruba_log", viewModel.userInfo.name)
}
}
日志结果:
I/aruba_log: 张三
5.@Qualifier注解解决注入冲突
- 对于构造函数注入,只能有一个构造函数被@Inject注解,否则编译时报错
- 对于模块注入,如果多个@Provides注解的方法返回相同类型,使用@Qualifier注解可以解决冲突,@Qualifier注解相当于为其取了个别名,在使用对象注入时也相应的使用@Qualifier注解,即可得到对应的注入对象
5.1 @Named解决注入冲突
@Named注解源码中,使用了@Qualifier元注解:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
模块代码如下:
@InstallIn(ViewModelComponent::class)
@Module
class UserInfo2Fetcher {
@Named("zhao")
@ViewModelScoped
@Provides
fun provideUserInfo(): UserInfo2 {
return UserInfo2("赵四")
}
@Named("wang")
@ViewModelScoped
@Provides
fun provideUserInfo2(): UserInfo2 {
return UserInfo2("王五")
}
}
UserInfo2:
class UserInfo2(val name: String)
ViewModel中,指定注入使用@Named("wang")
@HiltViewModel
class UserViewModel @Inject
constructor(
var userInfo: UserInfo,
@Named("wang") var userInfo2: UserInfo2
) : ViewModel()
最后在Activity中打印结果
Log.i("aruba_log", viewModel.userInfo2.name)
结果:
I/aruba_log: 王五
5.2 自定义注解解决冲突
我们也可以使用@Qualifier定义一个注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Zhao()
使用自定义的注解,解决冲突
@InstallIn(ViewModelComponent::class)
@Module
class UserInfo2Fetcher {
@Zhao
@ViewModelScoped
@Provides
fun provideUserInfo(): UserInfo2 {
return UserInfo2("赵四")
}
@Named("wang")
@ViewModelScoped
@Provides
fun provideUserInfo2(): UserInfo2 {
return UserInfo2("王五")
}
}
ViewModel中替换成自定义的注解后运行
@HiltViewModel
class UserViewModel @Inject
constructor(
var userInfo: UserInfo,
@Zhao var userInfo2: UserInfo2
) : ViewModel()
结果:
I/aruba_log: 赵四
四、接口注入
当我们有一个接口,并且有它的实现类,那么Hilt也可以注入生成该接口。这也是Dagger的功能
1.定义接口
interface ICallback {
fun onSuccess()
fun onFailure()
}
2.实现类,并使用@Inject注解
class CallbackImpl @Inject constructor() : ICallback {
override fun onSuccess() {
Log.i("aruba_log", "onSuccess")
}
override fun onFailure() {
Log.i("aruba_log", "onFailure")
}
}
3.定义模块抽象类,提供抽象方法返回ICallback接口
该方法需要入参为实现类,并使用@Binds注解
@InstallIn(ActivityComponent::class)
@Module
abstract class CallbackFetcher {
@Binds
abstract fun provideCallback(callback: CallbackImpl): ICallback
}
4.Activity中注入,并调用方法:
@AndroidEntryPoint
class ViewModelActivity : AppCompatActivity() {
@Inject
lateinit var callback: ICallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)
callback.onSuccess()
}
}
日志结果:
I/aruba_log: onSuccess
五、默认绑定
Hilt定义的组件都绑定了安卓上下文相关对象,如:在ActivityComponent中注入的类,直接可以通过注入获取Activity对象
以上面接口实现类为例子
1.构造方法中使用@ActivityContext注解注入Context
除了@ActivityContext,还可以通过@ApplicationContext注入全局上下文
这边在回调时,弹一个toast
class CallbackImpl @Inject constructor(@ActivityContext var context: Context) : ICallback {
override fun onSuccess() {
Log.i("aruba_log", "onSuccess")
Toast.makeText(context, "onSuccess", Toast.LENGTH_SHORT).show()
}
override fun onFailure() {
Log.i("aruba_log", "onFailure")
Toast.makeText(context, "onFailure", Toast.LENGTH_SHORT).show()
}
}
运行效果:
2.注入Activity
我们也可以直接定义一个Activity的成员变量,并通过构造函数注入
class CallbackImpl @Inject constructor(var activity: Activity) : ICallback {
override fun onSuccess() {
Log.i("aruba_log", "onSuccess")
Toast.makeText(activity, "onSuccess", Toast.LENGTH_SHORT).show()
}
override fun onFailure() {
Log.i("aruba_log", "onFailure")
Toast.makeText(activity, "onFailure", Toast.LENGTH_SHORT).show()
}
}
3.组件-绑定对应关系
组件下注入类可以获取对应的绑定对象
组件 | 创建时机 |
---|---|
SingletonComponent | Application |
ActivityRetainedComponent | Application |
ServiceComponent | Application、Service |
ActivityComponent | Application、Activity |
ViewModelComponent | Application |
FragmentComponent | Application、Activity、Fragment |
ViewComponent | Application、Activity、View |
ViewWithFragmentComponent | Application、Activity、Fragment、View |
总结
DI框架虽然带来了极大的便利,但无论是Dagger还是Hilt,在使用过程中,有必要使用流程图、UML图等来设计记录组件和作用域的绑定关系,以便后续使用与维护