Android 依赖注入 hilt 库的使用

3,654 阅读13分钟

hilt官网 hilt文档

一、背景

我们写程序大部分时候都是通过 new Object()来创建对象,当该对象需要在多个地方被创建时,我们有可能会封装成工厂方法。有没有更优雅的实现方式呢?
依赖注入相信大家都能说出个一二,但是在Android 端很少人会去用,下面通过一个小例子,来介绍依赖注入的使用,希望能为您带来一点帮助。先介绍下2个名词。

1.1、控制反转和依赖注入

IOC(控制反转):全称是 Inverse of Control , 是一种思想.指的是让第3方去控制去创建对象.

DI(依赖注入):全称是 Dependency Injection , 对象的创建是通过注入的方式实现. 是IOC的一种具体实现.

传统程序创建对象由调用者通过Obj obj = new Obj()创建,而依赖注入是把对象的创建和对象生命周期的管理交给容器来管理,定义注解标识,如:@Inject Obj obj,容器会根据注解在合适的时机自动为我们创建实例,我们在程序中直接使用obj。

二、 用依赖注入的目的是解耦

举个例子

在使用MVVM模式进行网络请求时,ViewModel 依赖 Repository 层,Repository 依赖Remote Data Source 和 Room,先忽略Room,把MainApi 当成 Remote Data Source,下面我分别用普通写法、泛型反射写法、hilt写法进行举例说明 image.png

2.1、普通写法

// 定义网络接口
interface MainApi {
     @GET("goods/list")
     List<String> requestList()
}

// 仓库
class MainRepo {
    private MainApi api;
    public MainRepo(MainApi api) {
        this.api = api;
    }
     List<String>  requestList() {
        // 具体调用接口
       return api.requestList();
    }
}

// ViewModel层
class MainViewModel extends ViewModel {
   private MainRepo repo = new MainRepo(new MainApi() {});

    void requestList(){
        // 通过repo请求接口
        List<String> list = repo.requestList();
    }
}

问题: 因为根据MVVM架构,每个Activity和Fragment都依赖ViewModel,每个ViewModel都依赖Repository,但是在ViewModel实例是通过谷歌提供的api创建的,在每一个ViewModel中通过new的方式进行创建Repo,这种代码是重复且不优雅的,通过反射可以减少这些重复代码。

2.2、反射写法

// 定义网络接口
interface MainApi {
     @GET("goods/list")
     List<String> requestList()
}

// 仓库抽象类
abstract class BaseRepo<Api> {
    private Api api;

    public Api getApi() {
        return api;
    }

    public void setApi(Api api) {
        this.api = api;
    }
}

// 首页仓库
class MainRepo extends BaseRepo<MainApi> {
    void requestList() {
        // 具体调用接口
        getApi().requestList();
    }
}

// 抽象ViewModel层
abstract class BaseViewModel<R extends BaseRepo> {
    private R repo;

    public BaseViewModel() {
        try {
            repo = crateRepoAndApi(this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public R getRepo() {
        return repo;
    }
    // 反射创建Repo和Api
    public R crateRepoAndApi(BaseViewModel<R> model) throws Exception {
        Type repoType = ((ParameterizedType) model.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        R repo = (R) repoType.getClass().newInstance();
        Type apiType = ((ParameterizedType) repoType.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        String apiClassPath = apiType.getClass().toString().replace("class ", "").replace("interface ", "");
        repo.setApi(Class.forName(apiClassPath));
        return repo;
    }
}

// ViewModel层
class MainViewModel extends BaseViewModel<MainRepo> {
    void requestList() {
        // 通过repo请求接口
        getRepo().requestList();
    }
}

新增了BaseRepo和BaseViewModel 2个类,并定义了泛型,在BaseViewModel实例化时获取子类的泛型,然后反射创建Repo,再根据Repo的泛型,反射创建api。通过反射确实能够解耦,并且不用在每个ViewModel手动创建Repo和Api了,有没有其他实现方式呢?

image.png

2.3、Hilt写法

@HiltViewModel
public class MainViewModel extends ViewModel {
    @Inject
    public MainRepo repo ;
}

class MainRepo extends BaseRepo {
    @Inject
    public MainRepo() {}
    
    @Inject
    public MainApi api;
}

@InstallIn(SingletonComponent.class)
@Module
public class ApiModule {
    @Singleton
    @Provides
    public MainApi provideMainApi() {
        // 通过Retrofit创建api,这里只是举例,
        return new MainApi() {};
    }
}

public interface MainApi {
     @GET("goods/list")
     List<String> requestList()
}

3种不同写法都讲完了,各有优势劣势,hilt写法虽然高级,但是在编译期新增了不少的类,模块化开发需要在每个模块的build.gradle添加依赖,用kotlin写法代码量会更少点,为了方便阅读我用Java代码举的例子。至于实战中用哪种方式来实现就仁者见仁了,我个人偏向于反射泛型实现,虽然在ViewModel中创建Repo不符合MVVM架构的思想,但是使用起来也方便和解耦。

三、hilt的具体使用步骤

在介绍hilt之前,先说下依赖注入在Android中的历史,Dagger是由square在2012年推出的,基于反射来实现的。后来谷歌在此基础上进行了重构,也就是Dagger2,基于Java注解来实现的,在编译期就会检查错误,如果编译通过,项目正常运行是没问题的,适用于Java、kotlin。而 hilt 则是谷歌面向Android写的一套依赖注入框架,相比Dagger2 简单易用,提供了android端专属api。

3.1、引入包

1 在项目最外层build.gralde引入
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'

2 在app模块顶部
plugin "dagger.hilt.android.plugin"
plugin "kotlin-kapt"

3 在app模块内 
kapt { // 纠正错误类型,可选
    correctErrorTypes true 
}
android {
 compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
 }
4 添加依赖
 implementation 'com.google.dagger:hilt-android:2.37'
 kapt 'com.google.dagger:hilt-compiler:2.37'

3.2、添加@HiltAndroidApp

必须在Application子类上添加注解@HiltAndroidApp

@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

sunflower这是谷歌Android官方的示例

四、Hilt 常用注解的含义

在没有使用依赖注入框架之前,我们在代码中创建对象正常是通过new xxx(),这个对象的创建过程其实是交给了虚拟机本身,我们不关心其内部是怎么创建的。而使用依赖注入后,对象的创建就交给 hilt 控制了,hilt内部是怎么创建的呢? 这时也引入一个概念: 容器。

容器负责创建对象,不需要手动我们去new,只需要在指定位置添加不同的注解,hilt内部选择不同的容器帮我们创建对象,怎么选择的呢?主要是由我们自己通过@InstallIn(容器.class)指定 ,容器创建的对象的生命周期由hilt决定,单例类就由单例容器创建,ViewModel容器创建出来的对象生命周期和ViewModel是一致的。在Activity中使用的对象就由Activity容器创建,不同容器创建的对象,其生命周期也不一样。

准确的应该叫组件,我喜欢叫容器,更加通俗点,哈哈。下面是官方的图,我在此基础上添加了ViewModelComponent,更加全面点。

容器/组件 (接口)作用范围(注解)创建于销毁于
SingletonComponent@SingletonApplication#onCreate()Application#onDestroy()
ActivityRetainedComponent@ActivityRetainedScopedActivity#onCreate()Activity#onDestroy()
ServiceComponent@ServiceScopedService#onCreate()Service#onDestroy()
ViewModelComponent@ViewModelScopedViewModel创建ViewModel销毁
ActivityComponent@ActivityScopedActivity#onCreate()Activity#onDestroy()
FragmentComponent@FragmentScopedFragment#onAttach()Fragment#onDestroy()
ViewComponent@ViewScopedView#super()View destroyed
ViewWithFragmentComponent@ViewScopedView#super()View destroyed

image.png image.png 通过上面图片我们要明白以下几点:

  1. 容器/组件:@InstallIn用在类上,作用范围: @Singleton、@ActivityScoped等用在方法上。
  2. SingletonComponent容器的生命周期是最长的,ServiceComponent 和 ActivityRetainedComponent 继承于SingletonComponent的ActivityComponent 和 ViewModelComponent 继承于 ActivityRetainedComponent,以此类推...
  3. 通过@ActivityScoped标识的方法返回的类,可以在Fragment和View中进行依赖注入,通过@FragmentScoped标识的类,如果是ViewWithFragmentComponent容器创建的对象也可以在View中注入。 问题: 在MainActivity注入一个User类时,hilt 内部是怎么创建对象的呢?理解这点非常重要。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var scope1: User
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
class User @Inject constructor() { }

@Module
@InstallIn(ActivityComponent::class)
class UserModule {

    @Provides
    @ActivityScoped
    fun createUser(): User {
        return User()
    }
 }

以上面代码举例:

  1. 当前注入类是在入口点Activity中声明的,hilt 内部会去依次寻找咱们项目中通过@installIn定义的ActivityComponent、ActivityRetainedComponent、SingletonComponent 3个容器。同理如果注入类是在Fragment声明的,hilt 内部会去依次寻找FragmentComponent、ActivityComponent、ActivityRetainedComponent、SingletonComponent。

  2. 寻找容器中创建User对象的方法,如果该方法添加了@Provides,则命中,通过该方法进行实例化。如果该方法添加了@ActivityScoped,那么在该Activity里的Fragment 和 自定义的View 中注入User类,都是和Activity里的user对象是同一个。

  3. 如果该方法没有添加@Provides注解,那么hilt 会找到User类,看User类的上方有没有@ActivityScoped注解,如果有,则通过User的构造实例化,那么在该Activity里注入多个User,其实都是同一个对象,在Fragment和View中注入的User,和Activity中的User也是同一个对象,因为User的作用域已经声明成和Activity同生命周期的,在Activity的onCreate()和onDestory()范围内只会创建一个user对象。

  4. 所以这就解释了为什么ActivityComponent除了指向FragmentComponent,也指向了ViewComponent,因为对象是由ActivityComponent容器创建的,并且在创建对象的方法上声明了@ActivityScoped注解,那么在Fragment和View中注入的对象,都会通过该容器创建。

  5. @xxScoped注解,可以定义在方法和类上,如果对象的创建是通过容器创建的,即使在类上面定义了@xxScoped注解,也会被 hilt 忽略。

@HiltAndroidApp

让 hilt 生效的必要条件,使用hilt的模块必须在Application的子类中声明@HiltAndroidApp

@InstallIn

作用在类名上面,如:@InstallIn(SingletonComponent::class),标识提供当前类创建的对象都通过该容器创建,具体使用见下面示例

@Module

作用在类名上面,一般和@InstallIn一起使用,标识当前类是一个模块,类中的方法会被指定的容器类创建。一半用来创建:第3方类,接口,build 模式的构造等。和@InstallIn 同时使用,指定该模块通过哪个容器创建。具体使用见下面示例

@Singleton

作用在方法上面,标识通过该方法创建的对象是单例,该方法只会执行一次。具体使用见下面示例

@Provides

作用在方法上面,标识方法返回的对象是通过容器创建的,方法主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行方法主体。具体使用见下面示例。

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

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit()
    }
}

  1. @InstallIn(容器.class),只能在()里写上面指定的容器.class
  2. @Module和@InstallIn是同时使用的,不加@Module编译不通过
  3. @Singleton 表示当前方法只会被执行一次,方法返回的对象是单例
  4. @Provides 作用在方法上是让容器在创建Retrofit对象的时候执行该方法,
@AndroidEntryPoint

作用在以下指定的类上面,除了Application Hilt 一共有 6 个入口点,分别是:
Application
Activity
Fragment
View
Service
BroadcastReceiver

除了Application由@HiltAndroidApp进行标识,Activity仅支持ComponentActivity的子类,Fragment仅支持androidx下的Fragment,其他入口点的子类都用@AndroidEntryPoint, 比如:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
// 错误写法,编译不通过
@AndroidEntryPoint
class User{}
@ActivityScoped

可以作用在类和方法上面。作用在类上面,当在 Activity 注入该 Activity


class UnscopedBinding @Inject constructor() {}

@ActivityScoped  
class ScopedBinding @Inject constructor() {}

@Module
@InstallIn(ActivityComponent::class)
class MainModule {
    @Provides
    fun provideUnscopedBinding() = UnscopedBinding()

    @Provides
    @ActivityScoped
    fun provideScopedBinding() = ScopedBinding()
}

@Inject
lateinit var unscope1: UnscopedBinding

@Inject
lateinit var unscope2: UnscopedBinding

@Inject
lateinit var scope1: ScopedBinding

@Inject
lateinit var scope2: ScopedBinding

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
// MyLinearLayout 是 activity_main 里面的一个控件
class MyLinearLayout : LinearLayout {
    ...
    @Inject
    lateinit var scope3: ScopedBinding
    @Inject
    lateinit var scope4: ScopedBinding
    
}

scope1、scope2、scope3、scope4 是同一个对象,因为在provideScopedBinding()加了@ActivityScoped,表示该方法创建出的对象的生命周期在Activity范围内时,实例只会创建一次,MyLinearLayout的context也是MainActivity,所以 scope1 == scope3 == scope4

unscope1、unscope2、unscope3、unscope4 是4个不同的对象
@Provides 作用在方法上是让容器在创建对象的时候执行该方法, 如果不加@Provides,那么容器创建对象不会走provideScopedBinding()方法。 @Module和@InstallIn是同时使用的,不加@Module编译不通过

@ViewModelScoped
@FragmentScoped
@ViewScoped
@ServiceScoped

@xxScoped注解,可以定义在方法和类上,如果对象的创建是通过容器创建的,即使在类上面定义了@xxScoped注解,也会被 hilt 忽略。如果定义在类上面,并且对象的创建没有通过容器,那么类上面的@xxScoped注解会生效。

@Inject

使用 @Inject 来告诉 Hilt 创建该类的实例,常用于构造,非私有字段,非静态方法中。

// 作用在构造
class User @Inject constructor() {
    
    // 作用在非静态方法中
    @Inject
    fun autoCallByHilt(){
        // 当User对象创建完成,hilt 会自动调用该方法
    }
}
  
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // 作用在非私有字段
    @Inject
    lateinit var user: User
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

@EntryPoint

hilt 已经默认支持6个入口点Application、Activity、Fragment、View、Service、BroadcastReceiver。@EntryPoint 是hilt提供给我们自定义的入口点,必须和@InstallIn同时使用。

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    // 必须得通过容器实例化Retrofit,否则编译不通过
    fun getRetrofit(): Retrofit
}

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit()
    }
}

  
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
   ...
  fun doSomething(context: Context) {
    val myEntryPoint = EntryPoints.get(context, MyEntryPoint::class.java)
    val retrofit = myEntryPoint.getRetrofit()
    ...
  }
}

@Binds 和 @Qualifier

必须作用在一个抽象方法上,抽象方法返回的必须是接口

// 定义一个水果模块,模块里方法的返回对象由Activit容器创建
@Module
@InstallIn(ActivityComponent::class)
abstract class FruitModule {
    // 苹果注解标识
    @AppleAnnotation  
    @Binds  
    abstract fun provideApple(pear: Apple): Fruit

    // 梨子注解标识
    @PearAnnotation
    @Binds
    abstract fun providePear(apple: Pear): Fruit
}
// 定义苹果注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppleAnnotation

// 定义梨子注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PearAnnotation

// 定义水果类
interface Fruit {
    fun getName(): String
}

class Apple @Inject constructor() : Fruit {
    override fun getName(): String {
        return "苹果"
    }
}

class Pear @Inject constructor() : Fruit {
    override fun getName(): String {
        return "梨子"
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @AppleAnnotation
    @Inject
    lateinit var apple: Fruit

    @PearAnnotation
    @Inject
    lateinit var pear: Fruit
    
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        System.out.println(apple.getName()) // 苹果
        System.out.println(pear.getName()) // 梨子
    }
}
  1. @Binds只能作用在抽象方法上,并且抽象方法里的参数必须是具体类,返回值必须是接口
  2. @Qualifier作用在自定义的注解上,自定义注解和@Binds共同作用在抽象方法上,hilt 会根据方法里的参数对象,创建具体实例。
  3. 如果接口只有一个子类,就不需要用自定义注解和@Qualifier

五、如何注入接口

见上面@Binds 和 @Qualifier的示例

六、如何注入第3方类

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

  @Provides
  @SingleScoped
  fun provideAnalyticsService(
  ): Retrofit {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
  }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var retrofit: Retrofit
    ...
} 

定义@InstallIn注解的时候 , 记得加上 @Module,在创建第3方对象的方法上必须得加上@Provides , 告诉hilt 通过该方法创建对象,而不是通过第3方类的构造方法。@SingleScoped 注解表示该方法只会被执行一次,创建出来的对象是单例。

七、如何注入同一个接口的不同子类

见上面@Binds 和 @Qualifier的示例

八、hilt 内部 组件/容器 默认绑定

image.png

意思是:被注入的类,在构造方法中可以默认持有不同对象的引用,持有的对象根据当前入口点进行分析。具体见示例。

// context、act、frag、view、会被 hilt 赋值
class User @Inject constructor(var context: Application)

class UserByAct @Inject constructor(var act: FragmentActivity) 

class UserByFragment @Inject constructor(var frag: Fragment) 

class UserByView @Inject constructor(var view: View) 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    // 错误写法,当前入口点无法把View注入进UserByView类中
    @Inject
    lateinit var userView: UserByView
    
   // 错误写法
    @Inject
    lateinit var userByFrag: UserByFragment
    
    ...
} 

@AndroidEntryPoint
class MyFragment : Fragment() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    @Inject
    lateinit var userByFrag: UserByFragment
}

@AndroidEntryPoint
class MyView : View {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    // 正确
    @Inject
    lateinit var userView: UserByView
    
    // 错误写法
    @Inject
    lateinit var userByFrag: UserByFragment
    ...
} 

也可以把Application转换成我们自己的 MyApplication

@Module
@InstallIn(SingleComponent::class)
object ApplicationModule {
    @Provides
    fun provideMyApplication(context: Application): MyApplication {
        return context as MyApplication
    }
}

@Module
@InstallIn(FragmentComponent::class)
class BaseFragmentModule {
    @Provides
    fun provideBaseFragment(fragment: Fragment): BaseFragment {
        return fragment as BaseFragment
    }
}

class User @Inject constructor(var context: MyApplication) 

class UserByFragment @Inject constructor(var frag: BaseFragment) 

九、注入ViewModel


@HiltViewModel
class MyViewModel @Inject constructor( val repo: MyRepo) : ViewModel() 

class MyRepo @Inject constructor()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
   // 注意: 这里ViewModel的创建还是得交给ViewModelProvider进行创建,不能new 
  val vm by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
  
}

只需要在ViewModel的上面添加@HiltViewModel , 创建ViewModel的方式得通过ViewModelProvider

十、结尾

hilt 的常用用法也基本讲完了,引入hilt会默认把dagger库也引入进来,想要快速上手hilt,得先理解 hilt 容器创建对象的方式,每种注解的用法。在多模块的应用中使用依赖注入,还得借助 dagger中的api来实现。 目前hilt 和 Jetpack的集成只支持 ViewModel 和 WorkManager,相信谷歌未来会对hilt提供更多的Jetpack组件支持。