马上入职新公司了,新公司使用了Hilt,所以这几天先熟悉一下。Hilt是对Dagger的封装,所以还得先把Dagger练习一下。
这俩都是依赖注入框架,依赖注入是基于控制反转的原则,控制特定代码的执行也就是控制对象的创建,由外部容器创建对象,外部容器创建好之后,在注入到当前对象里面。
依赖注入是一种广泛应用的编程原则,遵循这种规范可以让代码更加的灵活,比如下面几个方面
- 解耦:对象的创建依赖于依赖注入的容器, 如果某个对象在很多地方使用,某天该对象改变了,或者实现类改变了,只需改变容器里的生成方式即可。
- 方便重构:项目中的网络框架、图片框架等都可以通过依赖注入来一键切换。
- 易于测试:测试的时候经常会mock假的对象,如果某段代码使用的对象本身就是从外部传过来的,那么mock一个对象传入进入就可以测试,加入是内部new的对象就没这么方便了。
依赖注入的方式有下面几种:
- 通过构造方法注入
- 通过setter方法注入
- 通过第三方类库动态注入,主要有两种方式:运行时动态方案,通过注解加反射的方式运行时生成依赖。编译时静态方案,在编译时生成连接依赖的代码。Dagger就是使用的第二种方案。
我们得先会用然后在去研究其实现原理,下面通过官方举的一个小例子来感受一下Dagger和Hilt的最基础的用法。这个小例子的功能是:
LoginActivity
=> LoginViewPresenter
=> LoginRepository
=> LoginLocalDatasource和LoginRemoteDatasource
就是一个LoginActivity通过LoginPresenter获取本地数据和远程数据的过程
Android中Dagger的用法
导包
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}
通过@Inject注解告知Dagger如何创建当前对象
这里我们先把数据类和presenter类创建出来
class LoginPresenter @Inject constructor(
private val loginRepository: LoginRepository
) {
fun getData(){
loginRepository.getData()
}
}
class LoginRepository @Inject constructor(
private val localDatasource: LoginLocalDatasource,
private val loginRemoteDatasource: LoginRemoteDatasource
){
fun getData() {
localDatasource.getLocalData()
loginRemoteDatasource.getRemoteData()
}
}
class LoginLocalDatasource @Inject constructor() {
fun getLocalData() {
Log.d("getData","本地数据来啦")
}
}
class LoginRemoteDatasource @Inject constructor() {
fun getRemoteData() {
Log.d("getData","远程数据来啦")
}
}
通过@Component注解告诉Dagger哪些类需要注入
@Component
interface ApplicationComponent {
fun injectLoginActivity(activity: LoginActivity)
fun injectOtherctivity(activity: Otherctivit)
...
}
@Component作用在接口上,其内部的方法不支持多态,也就是说加入我们有多个activity需要注入,那就有几个就写几个类似的方法。
在application中拿到全局的Component组建对象供别处使用
class MyApp:Application() {
val applicationComponent = DaggerApplicationComponent.create()
}
在LoginActivity中完成对象的注入并使用
class LoginActivity:AppCompatActivity() {
@Inject
lateinit var loginPresenter: LoginPresenter
override fun onCreate(savedInstanceState: Bundle?) {
(applicationContext as MyApp).applicationComponent.injectLoginActivity(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginPresenter.getData()
}
}
需要注意的是注入方法的调用需要写在super.onCreate()
之前,避免出现Fragment恢复的问题。在 super.onCreate()
中的恢复阶段,Activity 会可能需要访问 Activity 绑定的 Fragment。
这时候通过@Inject注解注入一个loginPresenter对象,就可以在当前activity中直接使用了。
what! 为了创建一个对象竟然做了这么多的工作,这也太麻烦了吧,也正因为如此,也劝退了很多人,
不过由于依赖注入框架的优点也很多,谷歌为了让我们在应用中使用以来注入框架也是煞费苦心,Dagger难用,那就在简化一下吧,于是Hilt出现了。
Android中Hilt的基本用法
项目的gradle文件中引入gradle插件
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
app.gradle中引入
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
获取数据的类跟Dagger中的都是一样的这里就略过了,从别的不同的步骤开始
给application添加@HiltAndroidApp注解
此注解会触发Hilt 的代码生成操作
@HiltAndroidApp
class MyApp : Application() {}
在需要注入的类上添加@AndroidEntryPoint
此注解告诉Hilt注入点在哪里比如下面:
@AndroidEntryPoint
class LoginActivity:AppCompatActivity() {
@Inject
lateinit var loginPresenter: LoginPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginPresenter.getData()
}
}
到这里我们就可以拿到loginPresenter对象自由的进行操作了。
跟前面的Dagger的使用对比一下可以发现,Hilt主要省去了我们手动写@Component组件相关的代码,在导入Hilt包的时候,我们发现引入了一个gradle插件hilt-android-gradle-plugin
,谷歌在这里面做了一些文章,给我们自动插入了注入的代码。
到这里dagger和Hilt的最基本的用法就完成了,可以看到Hilt使用起来还是非常简单的。
Dagger 和 Hilt 其他用法
Dagger模块注入
前面通过在构造方法中添加@Inject
注解来告诉dagger如何创建一个对象的实例,还有一种方法是通过dagger中的module模块来创建。就比如我们想要初始化一个第三方库中的对象,我们不可能去给它的构造方法天加@Inject
注解,那就只能使用这种方式了。
@Module
class MyModule {
@Provides
fun httpObject(): HttpObject{
return HttpObject()
}
@Provides
fun httpDb(): DbObject{
return DbObject()
}
}
class HttpObject {
fun doHttp(){
Log.i("doHttp","doHttp method handle")
}
}
class DbObject {
fun doDb(){
Log.i("doDb","doDb method handle")
}
}
@Module
注解告诉dagger这是一个module模块,其内部配合@Provides注解来使用 @Provides注解告诉dagger如何创建某个对象的实例
为了让dagger知道我们自定义的这个模块,需要在@Component
注解后面加上modules
参数,该参数是个数组,我们可以添加多个不同的module。
@Component(modules = [MyModule::class])
interface ApplicationComponent {
fun injectLoginActivity(activity: LoginActivity)
fun injectMainActivity(activity: MainActivity)
}
官方建议尽量使用
@Inject
注解来告知dagger如何创建一个对象,当该注解不能满足的时候在使用module,比如我们在创建某个对象之前需要做一些别的计算操作。或者一些第三方库无法通过构造方法来注入。因为每当需要提供某个类的实例的时候,dagger都会运行@Provides
注解修饰的函数
dagger 的作用域
作用域可以将某个实例限定在特定的生命周期内,比如我们前面的HttpObject实例我们想要全局唯一,可以使用dagger自带的@Singleton
注解来修饰Component和Module,我们也可以自定义不同名称的注解,比如命名为@ApplicationScope
@Scope
@Documented
@Retention(RUNTIME)
public @interface ApplicationScope { }
使用构造函数注入(通过 @Inject)时,应在类上添加作用域注释;使用 Dagger 模块时,应在 @Provides 方法中添加作用域注释。比如下面带代码
@Singleton
@Component(modules = [MyModule::class])
interface ApplicationComponent {
fun injectLoginActivity(activity: LoginActivity)
fun injectMainActivity(activity: MainActivity)
}
@Singleton
class LoginLocalDatasource @Inject constructor() {
fun getLocalData() {
Log.d("getData","本地数据来啦")
}
}
@Singleton
@Provides
fun httpObject(): HttpObject{
return HttpObject()
}
Hilt模块注入
跟dagger一样,当单纯的构造方法无法满足注入的时候,Hilt也提供了module模块来完成注入,跟dagger不同的是,Hilt需要在添加一个@InstallIn
注解来告诉dagger该模块需要运行在哪个Android类中,比如:
@Module
@InstallIn(ActivityComponent::class) // 以告知 Hilt 每个模块将用在或安装在哪个 Android 类中。
object MyModule {
@Provides
fun httpObject(): HttpObject{
return HttpObject()
}
@Provides
fun httpDb(): DbObject{
return DbObject()
}
}
因为Hilt是专门给Android设计的,Hilt中提供了一些可供选择的Android类,我们可以根据自己的需求来选择不同作用域的对象。
Hilt 组件 | 注入器面向的对象 | 创建时机 | 销毁时机 |
---|---|---|---|
ApplicationComponent | Application | Application#onCreate() | Application#onDestroy() |
ActivityRetainedComponent | ViewModel | Activity#onCreate() | Activity#onDestroy() |
ActivityComponent | Activity | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View | View#super() | 视图销毁时 |
ViewWithFragmentComponent | 带有 @WithFragmentBindings 注释的 View | View#super() | 视图销毁时 |
ServiceComponent | Service | Service#onCreate() | Service#onDestroy() |
Hilt注入接口
Hilt还可以通过@Bind
注解注入接口,比如
@Module
@InstallIn(ActivityComponent::class)
abstract class HttpModule {
@BindOkhttp
@Binds
abstract fun bindOkhttp(okhttpRequest: OkhttpRequest):IHttpRequest
@BindVolley
@Binds
abstract fun bindVolley(volleyRequest: VolleyRequest):IHttpRequest
}
interface IHttpRequest {
fun get()
}
class OkhttpRequest @Inject constructor(@ApplicationContext context: Context)
:IHttpRequest{
override fun get() {
Log.d("IHttpRequest","OkhttpRequest handle")
}
}
class VolleyRequest @Inject constructor(@ActivityContext context: Context)
:IHttpRequest{
override fun get() {
Log.d("IHttpRequest","VolleyRequest handle")
}
}
我们有一个接口和一些接口的实现类,通过@Binds
注解,dagger在运行的时候,就会把参数中的实现类创建出来赋值给接口实例。我们就可以在其他地方使用该实例了。
我们知道接口是可以多实现的,当我们的接口有多个实现类的时候,上面的代码中我们可能需要写多个@Binds
修饰的方法,那么如何区分要使用哪个实现类呢?Hilt中提供了自定义的注解限定符。这些限定符可以作用在@Binds
和@Provides
修饰的方法上。比如下面定义两个限定符
// 不同的限定符只需名字不一样即可
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindOkhttp()
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindVolley()
在最终使用的地方也添加上需要使用哪种实现类的注解即可。这种方式可以在隔离第三方框架的时候使用,比如上面的网络请求框架OkHttp和Volley,最终只需要在下面的MainActivity中修改一个注解就可以完成切换。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@BindOkhttp
@Inject
lateinit var httpRequest: IHttpRequest
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
httpRequest.get()
}
}
Hilt中还预定了一些默认的限定符,比如前面代码中的@ApplicationContext
和@ActivityContext
。如果我们类中的方法中需要这两个context,只需要加上这两个注解就可以使用了。
到这里dagger和Hilt的常用的方法就练习完啦,那它到底是怎么完成注入的呢
dagger的注入原理
首先前面我们导包的时候,通过kapt导入了dagger的注解处理器,所以我们知道dagger是在编译时通过注解处理器生成了一些辅助代码来创建对象。位置在/app/build/generated/source/kapt/debug文件夹里面
我们从前面的的HttpObject的例子看起
- 首先我们在application中调用了初始化的方法
val applicationComponent = DaggerApplicationComponent.create()
public static ApplicationComponent create() {
return new Builder().build();
}
public ApplicationComponent build() {
if (myModule == null) {
this.myModule = new MyModule();
}
return new DaggerApplicationComponent(myModule);
}
private DaggerApplicationComponent(MyModule myModuleParam) {
this.myModule = myModuleParam;
}
创建出MyModule的实例对象并保存在DaggerApplicationComponent类的成员变量中。
- 然后我们在MainActivity中调用了初始化的方法
(applicationContext as MyApp).applicationComponent.injectMainActivity(this)
injectMainActivity是我们自定义的Component接口中的一个方法,点进去之后会看到它现在已经自动有了一个实现的方法。
@Override
public void injectMainActivity(MainActivity activity) {
injectMainActivity2(activity);F
}
private MainActivity injectMainActivity2(MainActivity instance) {
MainActivity_MembersInjector.injectHttpObject(instance, HttpModule_HttpObjectFactory.httpObject(myModule));
return instance;
}
HttpModule_HttpObjectFactory.httpObject(myModule)
这句话点进去可以看到return Preconditions.checkNotNullFromProvides(instance.httpObject());
直接调用了了myModule的httpObject()方法,而这个方法恰好就是我们自定义的如何创建HttpObject对象的方法,最后返回需要创建的对象。
- 最后调用
MainActivity_MembersInjector.injectHttpObject
方法
@InjectedFieldSignature("com.yjt.daggertest.MainActivity.httpObject")
public static void injectHttpObject(MainActivity instance, HttpObject httpObject) {
instance.httpObject = httpObject;
}
将前面创建好的对象,赋值给activity的成员变量httpObject注入完成。
总结一下很简单:我们写一些辅助代码告诉dagger如何创建某个对象,使用的时候我们将activity对象传入到dagger容器中,dagger拿到activity的对象,调用我们事先写好的创建对象的方法创建出该对象,赋值给activity的成员变量完事。
Hilt的注入原理
通过前面的练习我们知道Hilt针对dagger的优化部分,主要是省去了我们自己去写Component这块,改为自动生成相关的代码。
首先我们来到注解处理器生成的文件夹/app/build/generated/source/kapt/debug里面,会看到这样的一个文件Hilt_MainActivity和MainActivity_GeneratedInjector这两个类
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
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;
((MainActivity_GeneratedInjector) this.generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
}
}
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
}
}
public interface MainActivity_GeneratedInjector {
void injectMainActivity(MainActivity mainActivity);
}
在该类继承自AppCompatActivity
它的构造方法中监听到Context可用的时候,调用inject()注入方法。该方法中又调用到了MainActivity_GeneratedInjector中injectMainActivity
方法。这个方法是不是很面熟,跟我们使用dagger的时候自己写的@Component类是一样的。
OK相关的类有了,我们在使用dagger的时候,会在activity的onCreate方法中写初始化注入的方法比如前面的(applicationContext as MyApp).applicationComponent.injectMainActivity(this)
,那Hilt是怎么将它生成的类跟我们的MainActivity结合到一起的呢?
这就用到了字节码插入的技术了,我们在引入Hilt的库的时候,在工程的根gradle文件中引入了一个hilt的gradle插件,这个插件就是用来插入代码的,下面来看一下插入后的文件。
我现在使用的gradle版本是7.0以上的版本,生成的文件位置在/app/build/intermediates/asm_instrumented_project_classes/debug文件夹下面。7.0以下的在/app/build/intermediates/transforms文件夹下面。
public final class MainActivity extends Hilt_MainActivity {
@Inject
public IHttpRequest httpRequest;
@NotNull
public final IHttpRequest getHttpRequest() {
IHttpRequest var1 = this.httpRequest;
if (var1 != null) {
return var1;
} else {
Intrinsics.throwUninitializedPropertyAccessException("httpRequest");
return null;
}
}
public final void setHttpRequest(@NotNull IHttpRequest var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.httpRequest = var1;
}
/** @deprecated */
// $FF: synthetic method
@BindOkhttp
public static void getHttpRequest$annotations() {
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131427357);
((TextView)this.findViewById(2131231121)).setOnClickListener(MainActivity::onCreate$lambda-0);
this.getHttpRequest().get();
}
private static final void onCreate$lambda_0/* $FF was: onCreate$lambda-0*/(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.startActivity(new Intent((Context)this$0, LoginActivity.class));
}
}
可以看到,通过修改字节码,将我们的MainActivity改为了继承自它自己生成的Hilt_MainActivity了。那么当MainActity创建的时候,Hilt_MainActivity的构造方法也会调用,最终在监听到Context可用的时候执行注入的代码。
OK 完工 到这里应该可以熟练的使用这两个框架啦哈哈哈