2

dagger and Hilt 从入门到入门

 2 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/122031913
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

dagger and Hilt 从入门到入门

专栏收录该内容
12 篇文章 0 订阅

马上入职新公司了,新公司使用了Hilt,所以这几天先熟悉一下。Hilt是对Dagger的封装,所以还得先把Dagger练习一下。

这俩都是依赖注入框架,依赖注入是基于控制反转的原则,控制特定代码的执行也就是控制对象的创建,由外部容器创建对象,外部容器创建好之后,在注入到当前对象里面。

依赖注入是一种广泛应用的编程原则,遵循这种规范可以让代码更加的灵活,比如下面几个方面

  1. 解耦:对象的创建依赖于依赖注入的容器, 如果某个对象在很多地方使用,某天该对象改变了,或者实现类改变了,只需改变容器里的生成方式即可。
  2. 方便重构:项目中的网络框架、图片框架等都可以通过依赖注入来一键切换。
  3. 易于测试:测试的时候经常会mock假的对象,如果某段代码使用的对象本身就是从外部传过来的,那么mock一个对象传入进入就可以测试,加入是内部new的对象就没这么方便了。

依赖注入的方式有下面几种:

  1. 通过构造方法注入
  2. 通过setter方法注入
  3. 通过第三方类库动态注入,主要有两种方式:运行时动态方案,通过注解加反射的方式运行时生成依赖。编译时静态方案,在编译时生成连接依赖的代码。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注解来修饰ComponentModule,我们也可以自定义不同名称的注解,比如命名为@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 组件注入器面向的对象创建时机销毁时机ApplicationComponentApplicationApplication#onCreate()Application#onDestroy()ActivityRetainedComponentViewModelActivity#onCreate()Activity#onDestroy()ActivityComponentActivityActivity#onCreate()Activity#onDestroy()FragmentComponentFragmentFragment#onAttach()Fragment#onDestroy()ViewComponentViewView#super()视图销毁时ViewWithFragmentComponent带有 @WithFragmentBindings 注释的 ViewView#super()视图销毁时ServiceComponentServiceService#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的例子看起

  1. 首先我们在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类的成员变量中。

  1. 然后我们在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对象的方法,最后返回需要创建的对象。

  1. 最后调用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_MainActivityMainActivity_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 完工 到这里应该可以熟练的使用这两个框架啦哈哈哈


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK