什么是依靠注入

依靠注入的英文是Dependency Injection,简称DI,做过Java开发的读者可能知道,Spring 结构中的控制回转功用便是经过依靠注入的办法来完成的。有个很有趣的现象,当我与许多Android开发者交流依靠注入技术的时分,他们总是急速逃避说:“依靠注入太难用了,我从来没有运用过依靠注入”。

真的是这样吗?事实是99%的Android开发者都在项目中运用过依靠注入却没有意识到,那么什么是依靠注入呢?

简单的说,一个类中运用的依靠类不是类本身创立的,而是经过结构函数或许特点办法设置的,这种完成办法就称为依靠注入。以手机需求刺进SIM卡才能够正常拨打电话为例,手机需求依靠SIM卡。这儿新建MobilePhone类和SimCard类,不运用依靠注入的完成办法,代码如下所示:

class SimCard {
    private val TAG = "SimCard"
    fun dialNumber() {
        Log.d(TAG, "拨打电话")
    }
}
class MobilePhone {
    fun dialNumber() {
        val simCard = SimCard()
        simCard.dialNumber()
    }
}

接着就能够调用MobilePhone类中的拨打电话办法了,代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val mobilePhone = MobilePhone()
    mobilePhone.dialNumber()
}

经过上面得代码能够知道,当调用MobilePhone的dialNumber办法时,首先在MobilePhone类的dialNumber办法中创立了SimCard目标,然后调用SimCard目标的dialNumber办法。这种完成办法MobilePhone类尽管依靠SimCard类,但运用时依靠类是MobilePhone类本身创立的,所以这种完成办法并没有运用依靠注入去完成。上面的比如假如运用依靠注入又该怎么完成呢?

很简单,首先修正MobilePhone类的dialNumber办法,代码如下所示:

class MobilePhone {
    fun dialNumber(simCard: SimCard) {
        simCard.dialNumber()
    }
}

这儿为dialNumber办法增加了一个SimCard类型的参数,运用参数调用SimCard类的dialNumber办法,Activity中的代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val mobilePhone = MobilePhone()
    val simCard = SimCard()
    mobilePhone.dialNumber(simCard)
}

在MainActivity中创立了SimCard类的实例,传给MobilePhone类的dialNumber办法运用,这种完成办法便是依靠注入。

在一般的完成办法中,MobilePhone类不只要担任本身类的功用,还要担任创立SimCard类,在MainActivity中创立MobilePhone类也是相同。运用依靠注入不只能够进步代码的可扩展性,还能够分离依靠项。上面演示代码中的依靠注入办法仅仅将依靠项的创立机遇放到了更上层,在实践开发中,类的依靠联系较为复杂,假如仍运用示例中的依靠注入办法就不太适宜了。Hilt是Google官方为开发者供给的能够简化运用的依靠注入结构。Hilt又是在Dagger的根底上开发的,所以在开始了解Hilt组件的运用办法之前,不得不来介绍一下Hilt与Dagger的联系。

从Dagger看Hilt

Dagger是Square公司开发的一个依靠注入结构。Dagger最初版本是选用反射的办法去完成的,信任开发者都知道,过多运用反射办法会影响程序的运转效率。因为反射办法在编译阶段是不会产生过错的,导致只要在程序运转时才能够验证反射办法是否正确。考虑到上述问题,Google根据Dagger开发了Dagger2,Dagger2是经过注解的办法去完成的,如此在编译时就能够发现依靠注入运用的问题。但Dagger2运用起来是比较繁琐的,因而能够掌握Dagger2并熟练运用的开发者并不多。而Hilt组件是根据Dagger开发、专门面向Android开发者的依靠注入结构,所以Hilt仅仅为依靠注入供给了更简洁的完成办法,而不是供给了依靠注入的能力。那么Hilt又该怎么运用呢?

Hilt的根本运用

增加依靠

相比较Jetpack其他组件而言,增加Hilt依靠项仍是略微有些复杂的。首先在根项目的build.gradle中增加Hilt插件,示例代码如下所示:

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

然后在app模块下的build.gradle中增加如下依靠项,代码如下所示:

plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
...
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Hilt当时支撑的Android类及其注解与留意事项如表所示。

Android类 注解 留意事项
Application @HiltAndroidApp 有必要界说一个Application
Activity @AndroidEntryPoint 仅支撑扩展ComponentActivity的Activity
Fragment @AndroidEntryPoint 仅支撑扩展androidx.Fragment的Fragment
View @AndroidEntryPoint /
Service @AndroidEntryPoint /
BroadcastReceiver @AndroidEntryPoint /

每个应用程序都包括一个Application,开发者能够经过自界说Application来做一些根底的初始化等操作。在运用Hilt时,开发者有必要自界说一个Application,并为其增加@HiltAndroidApp注解。这儿新建BaseApplication类承继自Application,并为其增加@HiltAndroidApp注解,代码如下所示:

@HiltAndroidApp
class BaseApplication : Application() {
}

将BaseApplication注册到配置文件中,代码如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example1.hiltdemo">
    <application
        android:name="com.example.BaseApplication"
...

这些准备工作做好后,首先来看怎么运用Hilt注入一般的目标。

依靠注入一般目标

新建UserManager类,供给获取Token的办法,代码如下所示:

class UserManager {
    val TAG = "UserManager"
    fun getUserToken() {
        Log.d(TAG, "获取用户token")
    }
}

当Activity中需求获取Token时,编写代码如下所示:

override fun onCreate(savedInstanceState: Bundle?) {
  ...
  val userManager = UserManager()
  userManager .getUserToken()
 }

运转程序,打印日志如图所示。

玩转Jetpack依赖注入框架——Hilt

上面的功用尽管能够正常运转,但所存在的问题也是一望而知的。MainActivity不只担任UI的显现,还创立了UserManager类。假如MainActivity的依靠类过多会导致MainActivity臃肿且难以保护,这个时分Hilt就该上场了。

Hilt经过为被依靠类的结构函数增加@Inject注解,来告知Hilt怎么供给该类的实例,修正UserManager类,代码如下所示:

class UserManager @Inject constructor() {
    val TAG = "UserManager"
    fun getUserToken() {
        Log.d(TAG, "获取用户token")
    }
}

接着为MainActivity中的UserManager依靠注入,代码如下所示:

 @AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
 lateinit  var userManager: UserManager
  override fun onCreate(savedInstanceState: Bundle?) {
...
    user.getUserToken()
    }
}

代码中首先为MainActivity增加 @AndroidEntryPoint注解,声明一个延迟初始化的UserManager变量并增加@Inject注解。运转程序,运转成果与图共同,这样MainActivity就经过依靠注入获取到了UserManager类的实例。这儿需求留意的是:由Hilt注入的字段如这儿的userManager不能为私有类型,不然会在编译阶段产生过错。

有些事务中要注入的目标可能存在参数,如8.1小节所示的MobilePhone类需求依靠SimCard类,这样就不能直接在MainActivity中注入MobilePhone类了。其实解决这个问题也很简单,MainActivity依靠MobilePhone类,MobilePhone类又依靠SimCard类,假如想让MobilePhone类依靠注入,则SimCard类也有必要依靠注入才能够,修正SimCard、MobilePhone类代码如下所示:

class MobilePhone @Inject  constructor ( val simCard: SimCard) {
    fun dialNumber() {
        simCard.dialNumber()
    }
}
class SimCard @Inject  constructor ( ) {
    private val TAG = "SimCard"
    fun dialNumber() {
        Log.d(TAG, "拨打电话")
    }
}

在MainActivity中注入MobilePhone,并调用dialNumber办法,代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var mobilePhone: MobilePhone
    override fun onCreate(savedInstanceState: Bundle?) {
...
        mobilePhone.dialNumber()
    }
}

运转程序,成果如图所示。

玩转Jetpack依赖注入框架——Hilt

如此一来就完成了带参数的依靠注入,但仍有些外部类无法经过结构函数去注入,如经常运用的OkHttp等第三方开源库,接下来来看怎么依靠注入第三方开源库。

依靠注入第三方组件

以OkHttp完成为例,这儿就不解说OkHttp的根底运用了,假如有不了解OkHttp的读者,可自行经过官网学习。运用OkHttp建议网络恳求时会创立OkHttpClient实例,编写代码如下所示:

var okHttpClient = OkHttpClient.Builder()
   .connectTimeout(10, TimeUnit.SECONDS)
   .build()

可是咱们不能将这部分代码字节写在Activity中,应当运用依靠注入的办法为MainActivity注入OkHttpClient目标。因为OkHttpClient类是第三方库的类导致开发者无法直接增加注解,这儿首先新建NetWorkUtil类,并增加@Module与@InstallIn注解,代码如下所示:

@Module
@InstallIn(ActivityComponent::class)
class NetWorkUtil {}

@Module注解表明这是一个用于供给依靠注入实例的模块。@InstallIn注解表明要装载到哪个模块中。这儿运用ActivityComponent表明要装载到Activity组件中,所以开发者能够在Activity、Fragment以及View中运用NetWorkUtil模块,假如还想在这三个组件之外运用NetWorkUtil模块,则需求装载到其他组件中,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()

假如想在应用大局中运用NetWorkUtil模块,则将InstallIn注解特点值修正为ApplicationComponent::class即可。

然后咱们在NetWorkUtil类中新增getOkHttpClient办法代码如下所示:

@Provides
fun getOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

这儿运用@Provides注解供给获取办法,这儿的办法名getOkHttpClient能够任意取,不影响运用,现在假如想在Activity中运用OkHttpClient可编写代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var okHttpClient: OkHttpClient
    override fun onCreate(savedInstanceState: Bundle?) {
        ... 
        okHttpClient.newCall(request).enqueue(object : Callback {
              ...
        })
    }
}

这样开发者就不需求在Activity中创立OkHttpClient目标了,不过现在的代码仍是存在问题的。一般情况下开发者都会将OkHttpClient目标设置为单例模式的,即大局只要一个OkHttpClient目标,解决这个问题需求将InstallIn特点值设置为ApplicationComponent::class并且为getOkHttpClient办法增加@Singleton注解,修正后的代码如下所示:

@Module
@InstallIn(ApplicationComponent::class)
class NetWorkUtil {
 @Singleton
 @Provides
 fun getOkHttpClient(): OkHttpClient {
      var okHttpClient = OkHttpClient.Builder()
           .connectTimeout(10, TimeUnit.SECONDS)
           .build()
      return okHttpClient
  }
}

这样在任意地方调用getOkHttpClient办法都只会创立一个OkHttpClient目标。@Singleton注解是Application组件类的效果域,Hilt只为绑定效果域限定到的组件的每个实例,创立一次限定效果域的绑定,对该绑定的所有恳求同享同一实例。各组件对应效果域联系如表所示。

组件名称 效果域
ApplicationComponent @Singleton
ActivityRetainedComponent @ActivityRetainedScope
ActivityComponent @ActivityScoped
FragmentComponent @FragmentScoped
ViewComponent @ViewScoped
ViewWithFragmentComponent @ViewScoped
ServiceComponent @ServiceScoped

这儿需求留意的是,绑定的效果域有必要与其安装到的组件的效果域共同,不然在运转程序时会产生反常。

上面代码创立的OkHttpClient目标设置的超时时刻是10秒钟,在实践事务开发中可能还会配置许多其他特点,如增加拦截器等,这儿以修正超时时刻是20秒钟为例,该怎么再供给一个超时时刻为20秒钟的OkHttpClient目标呢?

相同的先增加一个getOtherOkHttpClient办法,并将超时时刻设置为20秒钟,代码如下所示:

@Provides
fun getOtherOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

运转程序,出现编译过错,过错如图所示。

玩转Jetpack依赖注入框架——Hilt

这个过错开发者也能够理解,因为在程序中声明了两个供给OkHttpClient实例的办法,在运用的时分Hilt并不知道要依靠注入哪个实例,这个时分就要用到Qualifier注解来解决这个问题了。Qualifier注解的效果便是为相同类型的类注入不同的实例,一起来看看具体该怎么完成。

新建QualifierConfig文件,代码如下所示:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OkHttpClientStandard
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherOkHttpClient

@Retention是用于声明注解的效果规模,这儿声明为AnnotationRetention.BINARY表明注解在编辑后将会被保存。这儿界说了两个OkHttpClientStandard和OtherOkHttpClient两个注解类,界说好后,将注解类运用在供给OkHttpClient实例的办法即可,代码如下所示:

@Singleton
@OkHttpClientStandard
@Provides
fun getOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}
@OtherOkHttpClient
@Provides
fun getOtherOkHttpClient(): OkHttpClient {
    var okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .build()
    return okHttpClient
}

在Activity中运用两个OkHttpClient实例的办法如下所示:

@OkHttpClientStandard
@Inject
lateinit var okHttpClient: OkHttpClient
@OkHttpClientStandard
@Inject
lateinit var otherOkHttpClient: OkHttpClient

信任读者对Hilt怎么依靠注入一般类和第三方类,已经非常了解了。此外,Hilt还集成了Jetpack组件,接下来来看Hilt怎么依靠注入架构组件。

依靠注入架构组件

当时Hilt仅支撑ViewModel组件和WorkManager组件,这儿以ViewModel组件来看怎么运用Hilt依靠注入ViewModel?ViewModel依靠及根底运用办法可参照第三章的内容。

首先在build.gradle中增加Hilt的扩展依靠,代码如下所示:

dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

新建一个MainViewModel,并为其结构办法增加@ViewModelInject注解,代码如下所示:

class MainViewModel @ViewModelInject constructor(
) : ViewModel() {}

这样,就能够经过和之前相同的办法来获取ViewModel目标了,代码如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    val mainViewModel by viewModels<MainViewModel>()
...
}

这儿能够运用和依靠注入之前运用相同的办法获取ViewModel的目标,都是Hilt自动为开发者处理好的,当然也能够运用依靠注入一般类的办法注入ViewModel的目标,代码如下所示:

class MainViewModel @Inject constructor(
) : ViewModel() {}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var mainViewModel: MainViewModel
    ...
}

可是这种办法改变了获取ViewModel的正常办法并不建议运用。这样就完成了Hilt依靠注入ViewModel的功用。