ETJava Beta | Java    注册   登录
  • 搜索:
  • Jetpack架构组件学习(5)——Hilt 注入框架使用

    发表于      阅读(1)     博客类别:Crawler     转自:https://www.cnblogs.com/stars-one/p/18364360
    如有侵权 请联系我们删除  (页面底部联系我们)  

    原文: Jetpack架构组件学习(5)——Hilt 注入框架使用-Stars-One的杂货小窝

    本篇需要有Kotlin基础知识,否则可能阅读本篇会有所困难!

    介绍说明

    实际上,郭霖那篇文章已经讲得比较明白了(具体参考链接都贴在下文了),这里简单总结下:

    如果按照之前我们的MVC写法,我们可以直接在activity中发起网络请求,但发起网络请求我们需要调用一个Api对象的具体方法,而Api对象只能在activity中进行创建

    这里activity和api对象实际上就是耦合关系,从客观上讲,我们activity不应该去负责创建一个api对象

    所以使用注入框架,相当于有了个中间人帮activity处理,至于是中间人直接找到api对象,或者是中间人进行创建api对象,activity都不关心,activity只知道去找中间人就能帮得到一个api对象

    优点

    学习之后,目前感觉到的优点:

    1. 可能大型项目,多module那种比较适合
    2. MMVM/MVI架构的app也比较适合
    3. 注入接口,方便不同逻辑实现

    可能就是接口注入可能有些用处,比如上面例子,假设我们网络框架刚开始用的是okhttp,但后期可能又会变更其他框架,我们可以考虑封装一个通用接口,然后使用依赖注入,后期更换其他网络框架只需要实现接口的对应方法即可

    依赖注入比较针对是MVVM/MVI架构的app,传统mvc结构,我直接一个单例object,也可以解决问题,好像也没啥必要?

    网上大多数说的都是解耦,方便后续测试,问题是我都不怎么写测试用例,实在无法感受到具体好处就是

    总之,目前学习这个只是因为很多开源项目都开始用上了,学了这个发现大概才能看得懂哈哈,也顺手做下记录了

    基本使用

    1.依赖引入

    注: 下面我使用的是ksl的gradle脚本

    项目的build.kts文件

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

    在app模块里的build.kts加上插件和依赖

    plugins {
        id("com.android.application")
        id("kotlin-kapt") // kotlin-kapt 插件
        id("dagger.hilt.android.plugin") // Hilt 插件
    }
    
    dependencies {	
    	implementation("com.google.dagger:hilt-android:2.48.1")
    	kapt("com.google.dagger:hilt-compiler:2.48.1")
    }
    
    //还有记得有下面此数据配置,不过一般都默认有
    android{	
    	compileOptions {
    		sourceCompatibility = JavaVersion.VERSION_1_8
    		targetCompatibility = JavaVersion.VERSION_1_8
    	}
    }
    

    我这里直接新版本as创建的新项目,已经使用了toml+ksl的方式,贴下图参考下:

    toml:

    build.gradle.kt

    app里的build.gradle.kt

    这里有个坑:

    就是ksp和kapt一起使用会导致编译失败,得设置插件不传递,及上面的build.gradle.kt截图

    2.application上加注解

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

    注意在清单文件中使用MyApplication对象哦!

    <application
    	android:name=".MyApplication"
    	//省略其他...
    />
    

    3.注入对象

    class MyApi @Inject constructor(){
    	fun sendApi(){
    		
    	}
    }
    
    
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
    	/**
    	 * 这里不能是private,且是懒加载的方式
    	 */
    	@Inject
    	lateinit var api: MyApi
    		
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    }
    

    补充说明

    我们注意到上面出现了3个注解

    • @HiltAndroidApp 在application上使用
    • @AndroidEntryPoint 在activity等类上使用此注解,下面有补充说明
    • @Inject 用来注入对象及标识需要注入实体

    其中@HiltAndroidApp是在application中使用的,而

    hilt有以下入口点:

    • Application
    • Activity
    • Fragment
    • View
    • Service
    • BroadcastReceiver

    也就是说,我们使用依赖注入功能只能在这几个类中

    对于application,我们使用@HiltAndroidApp注解,这步是必须的,否则依赖注入不会生效!

    而另外的@AndroidEntryPoint注解,在哪里你需要使用到@Inject注入对象,则需要将当前类标明上注解@AndroidEntryPoint,(如上个步骤中的代码示例)

    进阶使用

    1.带参实体注入

    给之前的MyApi添加个新的构造参数

    class MyApi @Inject constructor(val client:Client){
        fun sendApi(){
    
        }
    }
    
    class Client @Inject constructor(){
        fun config() {
            
        }
    }
    

    总结: 需要依赖注入的实体,如果有其他参数,则保证其他参数实体也是有依赖注入即可

    上面的MyApi,也可以写成下面这样:

    class MyApi @Inject constructor(){
    	@Inject
        lateinit var client: Client
    	
        fun sendApi(){
    
        }
    }
    

    2.接口类型注入

    import dagger.Binds
    import dagger.Module
    import dagger.hilt.InstallIn
    import dagger.hilt.android.components.ActivityComponent
    import javax.inject.Inject
    
    
    interface ClientInterface{
        fun config()
    }
    
    class MyClient @Inject constructor():ClientInterface{
        override fun config() {
            Log.d("ttt", "myclient config ")
        }
    }
    
    class MyApi @Inject constructor(){
    
        @Inject
        lateinit var clientInterface: ClientInterface
    
        fun sendApi(){
            clientInterface.config()
            Log.d("ttt", "send api")
        }
    }
    
    @Module
    @InstallIn(ActivityComponent::class)
    abstract class ClientModule {
        @Binds
        abstract fun createClient(myClient: MyClient): ClientInterface
    
    }
    

    上面定义一个ClientInterface接口,我们要注入一个此接口实现类MyClient

    得多加一个ClientModule类,依赖注入的时候会通过此类中的对应方法createClient来注入

    这里类和方法名都是可以随意,不过方法里的参数就是你要注入的接口实现类,返回则是接口类,还要注意该类是抽象类!

    关于@InstallIn注解,在下面章节会再次讲解,这里先跳过,先这样使用即可

    PS:当然这里可以也可以不是接口类型,改成抽象类应该也是可以的!

    3.相同类型不同实例注入

    在上面接口类型注入的代码上加入一个新的类进行注入

    class MyTwoClient @Inject constructor():ClientInterface{
        override fun config() {
            Log.d("ttt", "mytwoclient config ")
        }
    }
    
    @Module
    @InstallIn(ActivityComponent::class)
    abstract class ClientModule {
        @BindMyClient
        @Binds
        abstract fun createClient(myClient: MyClient): ClientInterface
    
    
    	/**
    	 * 注意这个方法名不能与上面createClient相同,否则编译会失败!
    	 */
        @BindMyTwoClient
        @Binds
        abstract fun createTwoClient(myClient: MyTwoClient): ClientInterface
    }
    
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class BindMyClient
    
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class BindMyTwoClient
    

    修改要注入对象的地方:

    class MyApi @Inject constructor(){
    
    	/**
    	 * 这里使用@BindMyTwoClient来标明我们要注入MyTwoClient实例
    	 */
        @BindMyTwoClient
        @Inject
        lateinit var clientInterface: ClientInterface
    
        fun sendApi(){
            clientInterface.config()
            Log.d("ttt", "send api")
        }
    }
    

    4.外部第三方实体注入

    这里说的第三方,指的是第三方库,由于库里基本封装好了,代码修改不像上面那么自由,可能还没有构造方法,那我们应该如何实现注入?

    这里是使用@Provides注解来实现

    import dagger.Module
    import dagger.Provides
    import dagger.hilt.InstallIn
    import dagger.hilt.android.components.ActivityComponent
    import javax.inject.Inject
    import javax.inject.Qualifier
    
    class MyApi @Inject constructor(){
    
    	/**
    	 * 指定MyClient,依赖注入最终会调用后面createNClient()方法生成对象
    	 */
        @Inject
        lateinit var clientInterface: MyClient
    
        fun sendApi(){
            Log.d("ttt", "send api")
        }
    }
    
    class MyClient {
        fun config() {
            Log.d("ttt", "myclient config ")
        }
    }
    
    @Module
    @InstallIn(ActivityComponent::class)
    class ClientModule {
        @Provides
        fun createNClient(): MyClient{
    		//这里方便延时,我直接通过创建一个实例了
            return MyClient()
        }
    }
    

    和上面接口类型注入不同,需要注意以下几点:

    1. ClientModule这个类不是抽象类了
    2. @Provides注解的那个方法,也不是抽象方法,且不用@Inject注解标明

    或者方法还能加个参数(依赖其他类):

    @Module
    @InstallIn(ActivityComponent::class)
    class ClientModule {
        @Provides
        fun createNClient(): MyClient{
            return MyClient()
        }
        
    	/**
    	 * myClient这个也会被自动注入上面我们的那个返回数据
    	 */
        @Provides
        fun createNewApi(myClient: MyClient): MyNewApi{
            return MyNewApi(myClient)
        }
        
    }
    
    class MyNewApi(myClient: MyClient)
    

    组件和组件作用域

    介绍

    上面有个@InstallIn,翻译就是安装到的意思

    @InstallIn(ActivityComponent::class): 就是把这个模块安装到 Activity 组件当中;

    如果我们在Service中使用@Inject注入,则编译时就会提示出错,原因是ActivityComponent已经限定只能在activity里使用

    当然,除了ActivityComponent这个组件,我们还有其他的组件可用,如下表

    Android 类 组件 作用域
    Application SingletonComponent @Singleton
    Activity ActivityRetainedComponent @ActivityRetainedScoped
    ViewModel ViewModelComponent @ViewModelScoped
    Activity ActivityComponent @ActivityScoped
    Fragment FragmentComponent @FragmentScoped
    View ViewComponent @ViewScoped
    带有 @WithFragmentBindings 注解的 View ViewWithFragmentComponent @ViewScoped
    Service ServiceComponent @ServiceScoped
    • @Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例
    • @ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例
    • @ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例
    • @ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致

    组件的生命周期

    生成的组件 创建时机 销毁时机
    SingletonComponent Application#onCreate() Application 已销毁
    ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
    ViewModelComponent ViewModel 已创建 ViewModel 已销毁
    ActivityComponent Activity#onCreate() Activity#onDestroy()
    FragmentComponent Fragment#onAttach() Fragment#onDestroy()
    ViewComponent View#super() View 已销毁
    ViewWithFragmentComponent View#super() View 已销毁
    ServiceComponent Service#onCreate() Service#onDestroy()

    依赖注入实现单例

    一般情况下,我们的api全局应该是单例模式,所以上面的可以改成下面代码:

    @Module
    @InstallIn(SingletonComponent::class)
    class ClientModule {
    
        @Singleton
        @Provides
        fun createNClient(): MyClient{
            return MyClient()
        }
    }
    

    上面的@Singleton这个是不可省略的,省略了相当于你使用的默认的组件,相当于每次注入都是新创建实例了!

    然后需要注意的是,下面几个错误的写法:

    @Module
    @InstallIn(SingletonComponent::class)
    class ClientModule {
    
        @ActivityScoped //错误,与当前组件的作用域不一致
        @Provides
        fun createNClient(): MyClient{
            return MyClient()
        }
    }
    
    @Module
    @InstallIn(ActivityComponent::class)
    class ClientModule {
    
        @Singleton //错误,与当前组件的作用域不一致
        @Provides
        fun createNClient(): MyClient{
            return MyClient()
        }
    }
    

    组件作用域除了在module里使用,还可以修饰构造函数

    @ActivityScoped
    class Hardware @Inject constructor(){
        fun printName() {
            println("I'm fish")
        }
    }
    

    表示Hardware在同个Activity,只会有一个实例

    组件的层次

    组件有层次的使用,比如上面的全局的api,我们可以在其他地方组件作用域进行注入或者Activity,fragment中使用注入,如下代码:

    @Module
    @InstallIn(SingletonComponent::class)
    class ClientModule {
    
        @Singleton
        @Provides
        fun createNClient(): MyClient{
            return MyClient()
        }
    }
    
    @ActivityScoped
    class MyNewApi @Inject constructor(myClient: MyClient)
    

    具体的关系结构层次如下图所示:

    注入application或Activity

    当我们构造函数需要传递application或Activity的时候,可以使用@ApplicationContext@ActivityContext 限定符。

    如下面代码:

    class AnalyticsServiceImpl @Inject constructor(
      @ApplicationContext context: Context
    ) : AnalyticsService { ... }
    
    // The Application binding is available without qualifiers.
    class AnalyticsServiceImpl @Inject constructor(
      application: Application
    ) : AnalyticsService { ... }
    
    class AnalyticsAdapter @Inject constructor(
      @ActivityContext context: Context
    ) { ... }
    
    // The Activity binding is available without qualifiers.
    class AnalyticsAdapter @Inject constructor(
      activity: FragmentActivity
    ) { ... }
    

    特殊用法

    似乎是自定义入口类,然后给application实现一个扩展方法

    @Module
    @InstallIn(SingletonComponent::class)
    object PlayServiceModule {
    fun Application.playerController(): PlayerController {
    return accessEntryPoint<PlayerControllerEntryPoint>().playerController()
    }
    

    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface PlayerControllerEntryPoint {
    fun playerController(): PlayerController
    }

    }
    

    与ViewModel联用

    为ViewModel添加 @HiltViewModel 注解,并在 ViewModel 对象的构造函数中使用 @Inject 注解

    @HiltViewModel
    class ExampleViewModel @Inject constructor(
      private val savedStateHandle: SavedStateHandle,
      private val repository: ExampleRepository
    ) : ViewModel() {
      ...
    }
    

    然后,带有 @AndroidEntryPoint 注解的 activity 或 fragment 可以使用 ViewModelProvider 或 by viewModels() KTX 扩展照常获取 ViewModel 实例:

    @AndroidEntryPoint
    class ExampleActivity : AppCompatActivity() {
      private val exampleViewModel: ExampleViewModel by viewModels()
      ...
    }
    

    参考