收藏无名之辈的Android之路

Android日志:Hilt

2021-10-12  本文已影响0人  搬码人

引入

在了解Hilt之前,我们先来了解一个重要的概念——依赖注入(以下来源:Android开发者文档

依赖注入(DI)是一种广泛用于编程的技术,非常适用于Android开发。遵循DI的原则可以良好的应用架构奠定基础。
实现依赖注入可以带来以下优势:1、重用代码;2、易于重构;3、易于测试

什么是依赖项注入

类通常需要引用其他类。例如Car类可能需要引用Engine类。这些必需类成为依赖项,在此实例中,Car类依赖于拥有Engine类的实例才能实现。

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

类通常有三种方式获取所需的对象:
1、类构造其所需的依赖项。在以上示例中,Car将创建并初始化自己的Engine实例。
2、从其他地方抓取。某些Android API(如getSystemService())的工作原理就是如此。
3、以参数形式提供。应用可以在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数。在以上示例中,Car构造函数将接收Engine作为参数。这就是所谓的依赖项注入,使用这种方法,我们可以获取并提供类的依赖项,而不必让类实例自行获取。

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

使用这种方法的好处显而易见:作为Car 的构造函数参数,可以轻松地进行拆卸并测试其他类(Engine的子类)。
Android中有两种主要的手动依赖项注入方式:

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

注:依赖想注入基于控制反转原则,根据该原则,通用代码控制特定的执行。

依赖性注入的替代方法

依赖项注入的替代方法是使用服务定位器(或者我们一般更喜欢叫工具类)。服务定位器设计模式还改进了类与具体依赖项的分离。可以创建一个名为服务定位器的类,该类创建和存储依赖项,然后按需提供这些依赖项。

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

服务定位器模式与依赖项注入在元素使用方式上有所不同。使用服务定位器模式,类可以控制并请求注入对象;使用依赖想注入,应用可以控制并主动注入所需对象。


手动实现依赖项注入的实战案例

这是Android开发平台提供的一个引入Hilt的一个例子,里面从最初的依赖注入讲起,逐步规范依赖注入模式,最后达到和Dagger相接近的案例。
https://developer.android.google.cn/training/dependency-injection/manual?hl=zh_cn
其中涉及到MVVM涉及模式架构的搭建,在我往期的实战项目中也有体现。

总结
依赖项注入对于创建可扩展且可测试的Android应用而言是一项适合的技术。将容器作为在应用的不同部分共享各个类实例的一种方式,以及使用工厂类创建各个类实例的集中位置。
当应用变大时,我们会发现我们的项目编写了大量样板代码(如工厂类),这可能容易出错。我们还必须自行管理容器的范围和生命周期,优化并舍弃不再需要的容器以释放内存。如果操作不当,可能会呆滞应用出现微小错误和内存泄漏。


使用Hilt实现依赖注入

Hilt是Android的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手动依赖项注入要求我们手动构造每个类机器依赖项,并借助容器重复使用和管理依赖项。
Hilt通过为项目中的每个Android类提供容器并自动管理其生命周期,提供了一种在应用中使用DI(依赖项注入)的标准方法。Hilt是在Dagger的基础上构建而成的。

添加依赖

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

如果这里在配置的时候报错,将单引号更改为双引号即可。

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

在Application中添加@HiltAndroidApp注解

所有使用Hilt的应用都必须包含一个带有@HiltAndroidApp注解的Application类。

@HiltAndroidApp
class MyApplication:Application() {
    
}

为什么需要一个Application并且需要添加注解@HiltAndroidApp?
@HiltAndroidApp会触发Hilt的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。
生成的这一Hilt组件会附加到Application对象的声明周期,并为其提供依赖项。此外,它也是应用的父组件,这意味着,其他组件可以访问它提供的依赖项。

将依赖注入Android类

在Application类中设置了Hilt且有了应用及组建后,Hilt可以为带有@AndroidEntryPoint的其他Android类提供依赖项

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

如果使用@AndroidEntryPoint为某个Android类添加注解,则必须为依赖于该类的Android类添加注解。例如,如果我们为某个Fragment添加注解,则必须为使用该Fragment的所有Activity添加注解。
注:由Hilt注入的字段不能为私有字段。会导致编译报错
@AndroidEntryPoint会为项目中的每个Android类生成一个单独的Hilt组件。这些组件可以从它们各自的父类接收依赖项。
如需从组件获取依赖项,需使用@inject注解执行字段注入:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapte
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Hilt注入的类可以有同样使用注入的其他基类。如果这些类是抽象类,则它们不需要@AndroidEntryPoint。


定义Hilt绑定

/**
 * 为了执行字段注入,Hilt需要知道如何从相应组件提供必要依赖项的实例 ->绑定
 * 绑定包含将某个类型的实例作为依赖项提供所需的信息。
 * 向Hilt提供绑定信息的方法是构造函数注入。在某个类的构造函数中使用@Inject注解,
 * 以告知Hilt如何提供该类的实例
 *
 * Student是MyAdapter的一个依赖项,所以Hilt必须知道如何提供Student的实例
 */
class MyAdapter @Inject constructor(private val student: Student) {
}

Hilt模块

有时,类型不能通过构造函数注入。发生这种情况的原因有很多。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以通过使用Hilt模块向Hilt提供绑定信息。
Hilt模块是一个带有@Module注解的类。 它会告知Hilt如何提供某些类型的实例。还必须使用@InstallIn为Hilt模块添加注解,以告知Hilt每个模块将用在或安装在哪个Android类中。
在Hilt模块中提供的依赖项可以在生成的所有与Hilt模块安装到的Android类关联的组件中使用。
注:由于Hilt的代码生成操作需要访问使用Hilt 的所有Gradle模块,因此编译Application类的Gradle模块还需要在其传递依赖项中包含您的所有Hilt模块和通过构造函数注入的类。

/**
 *@Description
 *@Author PC
 *@QQ 1578684787
 */

/**
 * 使用@Binds注入接口实例
 * 接口是无法通过构造函数注入的,而应该向Hilt提供绑定信息
 * -> 在Hilt模块内创建一个带有@Binds注解的抽象函数
 * @Binds 注解会告知Hilt在需要提供接口的实例时要使用哪种实现
 * 带有注解的函数回想Hilt提供以下信息:
 *    - 函数返回类型会告知Hilt函数提供哪个接口的实例。
 *    - 函数参数会告知Hilt要提供哪种实现。
 */

//1、需要一个接口
interface AnalyticService{
    fun analyticsMethods()
}


//2、接口实现
//Hilt也需要知道如何提供AnalyticsServiceImpl的实例
class AnalyticsServiceImpl @Inject constructor():AnalyticService {
    override fun analyticsMethods() {

    }

}

//@InstallIn(ActivityComponent::class) 意味着所有依赖项都可以在应用的Activity中使用,
// 同理还有ApplicationComponent
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsNodule{
    
    
    //3、注入接口实现
    //提供接口类型的返回值,体现多态的作用,返回值可以切换任意继承该接口的类的参数
    @Binds
    abstract fun bindAnalyticsService(
            analyticsServiceImpl: AnalyticsServiceImpl
    ):AnalyticService
}
/**如果某个类是由外部库提供 ->Retrofit、OkHttpClient、Room数据库等
 * 或者必须使用工具类的方式创建实例,也无法通过构造函数注入。
  *解决方法->
 * 可以通过告知Hilt如何提供此类型的实例:在Hilt模块类创建一个函数
 * 并使用@Provides为该函数添加注解。
 *
 *
 *带有注解的函数会向Hilt提供一下信息:
 *   - 函数返回类型会告知Hilt提供哪个类型的实例
 *   - 函数参数会告知Hilt相应类型的依赖类
 *   - 函数主体会告知Hilt如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt都会执行函数主体。
 *
  */
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule{
    @Provides
    fun provideAnalyticsService():AnalyticService{

        return Retrofit.Builder()
                .baseUrl("www.google.com")
                .build()
                .create(AnalyticService::class.java)
    }
}

为同一类型提供多个绑定

使用限定符(Qualifiers) ->自定义注解  来标识该类型的特定绑定。

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

这里以上述的例子继续为例

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

您可以通过使用相应的限定符为字段或参数添加注释来注入所需的特定类型:

// 作为另一个类的依赖
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// 作为一个构造函数注入类型的依赖
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// 需要被注入的类
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

系统提供的预定义限定符

Hilt 提供了一些预定义的限定符。例如,由于您可能需要来自应用或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

为Android类生成的组件

对于系统提供的每一个Android类,都有一个关联的Hilt组件,您可以再@InstallIn注解中引用该组件。每个Hilt组件负责将其绑定注入相应的Android类。
前面演示了如何再Hilt模块中使用ActivityComponent。
Hilt提供了以下组件:

Hilt组件 注入器面向的对象
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent 带有WithFragmentBindings注解的View
ServiceComponent Service

注意:Hilt不会为广播接收器生成组件,因为Hilt直接从ApplicationComponent注入广播接收器


组件作用域

默认情况下,Hilt中的所有绑定都未限定作用域 ->每当应用请求绑定时,Hilt都会创建所需类型的一个新实例。
不过,Hilt也允许将绑定的作用域限定为特定组件。Hilt只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。

Android类 生成的组件 作用域
Application ApplicationComponent @Singleton(单例)
ViewModel ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
带有@WithFragmentBindings注解的View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

在这里,使用@ActivityScoped将AnalyticsAdapter的作用域限定为ActivityComponent,Hilt会在相应Activity的真个生命周期内提供AnalyticsAdapter的同一实例:

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

注意:将绑定的作用域限定为某个组件的作用域的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。所以,在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例,或者绑定的创建成本很高,那么将帮的作用域限定为某个组件是一种恰当的做法。


组件默认绑定

每个Hilt组件都附带一组默认绑定,Hilt可以将其作为依赖项注入您自己的自定义绑定。请注意,这些绑定对应于常规Activity和Fragment类型,而不对应于任何特定子类。因为,Hilt会使用单个Activity组件定义来注入所有Activity。每个Activity都有此组件的不同实例。

Android组件 默认绑定
ApplicationComponent Application
ActivityRetainedComponent Application
ActivityComponent Application和Activity
FragmentComponent Application、
ViewComponent View
ViewWithFragmentComponent 带有WithFragmentBindings注解的View
ServiceComponent Service

可以使用@ApplicationContext或@ActivityContext获得应用或Activity的上下文绑定。

class AnalyticsServiceImpl @Inject constructor(
  @ApplicationContext context: Context
) : AnalyticsService { ... }

// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
  application: Application
) : AnalyticsService { ... }

参考资料:https://developer.android.google.cn/reference?hl=zh_cn

上一篇下一篇

猜你喜欢

热点阅读