Android--Hilt入门

作者: aruba | 来源:发表于2021-12-07 09:59 被阅读0次

    谷歌接管Dagger后,推出了自己的Hilt框架,Hilt基于Dagger做了一层封装,大大简化了Dagger的使用,定制了一系列规范,并支持Jetpack中部分组件,是一个专门为安卓开发的DI框架

    一、构造函数注入

    和Dagger相同,Hilt也分两种注入方式,以上篇Dagger中的代码为例子,来对比两个框架的使用区别

    1.gradle中配置依赖

    工程gradle中导入插件:

        dependencies {
            ...
            classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.4"
        }
    

    moudle中进行依赖:

    plugins {
        id 'com.android.application'
        id 'kotlin-android'
        id 'kotlin-kapt'
        id 'dagger.hilt.android.plugin'
    }
    
    dependencies {
        ...
        def hilt_version = "2.40.4"
        implementation "com.google.dagger:hilt-android:$hilt_version"
        kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    }
    
    2.使用@Inject注解定义需要注入的类
    /**
     * 模拟本地数据源
     */
    class LocalDataSource @Inject constructor()
    
    /**
     * 模拟远程数据源
     */
    class RemoteDataSource @Inject constructor()
    

    定义包装类DataSource,包含上面两个类,同样对构造函数使用@Inject注解

    /**
     * 数据源包装类
     * Created by aruba on 2021/12/4.
     */
    data class DataSource @Inject constructor(
        var remoteDataSource: RemoteDataSource,
        var localDataSource: LocalDataSource
    ) 
    
    3.使用@HiltAndroidApp注解Application,表示注入中间件

    Dagger使用的是@Component注解表示一个组件,上篇文章也提到过,一个项目对应一个Component就足够了,Hilt规范了Component

    @HiltAndroidApp
    class App : Application()
    
    4.在Activity中使用@Inject注解对象
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var dataSource: DataSource
    
    ...
    
    5.对Activity使用@AndroidEntryPoint注解

    在Dagger中需要调用Component的注入方法,Hilt中直接使用注解就可以实现注入

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var dataSource: DataSource
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            Log.i("aruba_log", dataSource.toString())
        }
    }
    

    日志结果:
    I/aruba_log: DataSource(remoteDataSource=com.aruba.hiltapplication.di.datasource.RemoteDataSource@f7f11fd, localDataSource=com.aruba.hiltapplication.di.datasource.LocalDataSource@743eef2)

    和Dagger相比,我们多导入了一个插件,此插件是利用Javassist,将编译后将@AndroidEntryPoint注解的Activity继承至自己生成的类


    下面是该例子生成的Hilt_MainActivity:

    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;
          // 调用component的注入方法
          ((MainActivity_GeneratedInjector) this.generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
        }
      }
    
      @Override
      public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
        return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
      }
    }
    

    二、模块注入

    1.依赖网络框架
    dependencies {
        ...
    
        implementation 'com.squareup.retrofit2:retrofit:2.9.0'
        implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
    }
    

    别忘了在Manifest.xml中添加权限

    2.定义Retrofit API
    interface BaiduApiService {
        @GET("/index.html")
        fun index(): Call<String>
    }
    
    3.定义模块
    • 和Dagger相同,使用@Moudle注解就可以表示一个模块,使用@Provides注解提供给Component生成注入对象的方法
    • 使用@InstallIn注解,指定该模块需要装载到哪些Component中,并且我们不必再定义组件了,Hilt预定义了我们移动开发中所需的组件和子组件

    这边指定其装载到SingletonComponent中,也就是全局APP中,旧版本的ApplicationComponent已废弃

    @InstallIn(SingletonComponent::class)
    @Module
    class NetworkModule {
    
        @Provides
        fun getBaiduApiService(): BaiduApiService {
            return Retrofit.Builder()
                .baseUrl("https://www.baidu.com")
                .addConverterFactory(ScalarsConverterFactory.create())
                .build().create(BaiduApiService::class.java)
        }
    }
    
    4.在Activity中使用
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var dataSource: DataSource
        @Inject
        lateinit var baiduApiService: BaiduApiService
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            Log.i("aruba_log", dataSource.toString())
    
            getIndex()
        }
    
        /**
         * 获取百度首页
         */
        private fun getIndex() {
            baiduApiService.index().enqueue(object : Callback<String> {
                override fun onResponse(call: Call<String>, response: Response<String>) {
                    findViewById<TextView>(R.id.tv_hello).text = response.body()
                }
    
                override fun onFailure(call: Call<String>, t: Throwable) {
                }
            })
        }
    }
    

    效果:


    三、预定义组件与作用域

    1.预定义的Component

    Hilt定义的组件为SingletonComponent,子组件在dagger.hilt.android.components包下


    这些组件对应的生命周期为:

    组件 创建时机 销毁时机
    SingletonComponent Application#onCreate() Application#onDestroy()
    ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
    ServiceComponent Service#onCreate() Service#onDestroy()
    ActivityComponent Activity#onCreate() Activity#onDestroy()
    ViewModelComponent ViewModel#super() ViewModel#clear()
    FragmentComponent Fragment#onAttach() Fragment#onDestroy()
    ViewComponent View#super() 视图销毁时
    ViewWithFragmentComponent View#super() 视图销毁时
    2.预定义的Scope

    Hilt定义的子组件作用域在dagger.hilt.android.scopes包下


    这些作用域都是和子组件一一对应的,组件的层级关系如下图:


    组件-作用域层级关系
    3.模块中使用作用域

    Hilt的作用域就简单很多了,因为它预定义了组件和子组件 ,同时又定义了这些组件对应的作用域,上面的例子中,如何保证只实例化一份?使用SingletonComponent对应的作用域@Singleton即可,使用方法也是和Dagger相同的

    @InstallIn(SingletonComponent::class) //表示全局组件
    @Module
    class NetworkModule {
    
        @Singleton
        @Provides
        fun getBaiduApiService(): BaiduApiService {
            return Retrofit.Builder()
                .baseUrl("https://www.baidu.com")
                .addConverterFactory(ScalarsConverterFactory.create())
                .build().create(BaiduApiService::class.java)
        }
    }
    

    在Activity中,注入两个对象,并打印下两者的hashcode:

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
        @Inject
        lateinit var baiduApiService1: BaiduApiService
        @Inject
        lateinit var baiduApiService2: BaiduApiService
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            Log.i("aruba_log","baiduApiService1 hashcode:${baiduApiService2.hashCode()}")
            Log.i("aruba_log","baiduApiService2 hashcode:${baiduApiService2.hashCode()}")
        }
    }
    

    日志结果:
    I/aruba_log: baiduApiService1 hashcode:174572891
    I/aruba_log: baiduApiService2 hashcode:174572891

    4.构造方法使用作用域

    ViewModelComponent是新出的子组件,对应的作用域为ViewModelScope,作用为:一个ViewModel中多个同类型注入对象,则使用同一份实例。以前实现ViewModel中注入还需要依赖其他框架,这次来使用ViewModelScope作为例子

    4.1 定义注入类,并使用@ViewModelScope注解

    注意:如果把参数放入主构造,并且赋了默认值,那么会生成两个构造方法,Hilt就会报错

    @ViewModelScoped
    class UserInfo @Inject constructor() {
        var name: String = "张三"
    }
    
    4.2 定义ViewModel,并为其添加@HiltViewModel注解

    在构造中注入对象

    @HiltViewModel
    class MainViewModel @Inject constructor(var userInfo: UserInfo) : ViewModel()
    
    4.3 Activity中使用
    @AndroidEntryPoint
    class ViewModelActivity : AppCompatActivity() {
        val viewModel: MainViewModel by lazy {
            ViewModelProvider(this).get(MainViewModel::class.java)
        }
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_view_model)
    
            Log.i("aruba_log", viewModel.userInfo.name)
        }
    }
    

    日志结果:
    I/aruba_log: 张三

    5.@Qualifier注解解决注入冲突
    • 对于构造函数注入,只能有一个构造函数被@Inject注解,否则编译时报错
    • 对于模块注入,如果多个@Provides注解的方法返回相同类型,使用@Qualifier注解可以解决冲突,@Qualifier注解相当于为其取了个别名,在使用对象注入时也相应的使用@Qualifier注解,即可得到对应的注入对象
    5.1 @Named解决注入冲突

    @Named注解源码中,使用了@Qualifier元注解:

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Named {
    
        /** The name. */
        String value() default "";
    }
    

    模块代码如下:

    @InstallIn(ViewModelComponent::class)
    @Module
    class UserInfo2Fetcher {
    
        @Named("zhao")
        @ViewModelScoped
        @Provides
        fun provideUserInfo(): UserInfo2 {
            return UserInfo2("赵四")
        }
    
        @Named("wang")
        @ViewModelScoped
        @Provides
        fun provideUserInfo2(): UserInfo2 {
            return UserInfo2("王五")
        }
    }
    

    UserInfo2:

    class UserInfo2(val name: String)
    

    ViewModel中,指定注入使用@Named("wang")

    @HiltViewModel
    class UserViewModel @Inject
    constructor(
        var userInfo: UserInfo,
        @Named("wang") var userInfo2: UserInfo2
    ) : ViewModel()
    

    最后在Activity中打印结果

    Log.i("aruba_log", viewModel.userInfo2.name)
    

    结果:
    I/aruba_log: 王五

    5.2 自定义注解解决冲突

    我们也可以使用@Qualifier定义一个注解

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

    使用自定义的注解,解决冲突

    @InstallIn(ViewModelComponent::class)
    @Module
    class UserInfo2Fetcher {
    
        @Zhao
        @ViewModelScoped
        @Provides
        fun provideUserInfo(): UserInfo2 {
            return UserInfo2("赵四")
        }
    
        @Named("wang")
        @ViewModelScoped
        @Provides
        fun provideUserInfo2(): UserInfo2 {
            return UserInfo2("王五")
        }
    }
    

    ViewModel中替换成自定义的注解后运行

    @HiltViewModel
    class UserViewModel @Inject
    constructor(
        var userInfo: UserInfo,
        @Zhao var userInfo2: UserInfo2
    ) : ViewModel()
    

    结果:
    I/aruba_log: 赵四

    四、接口注入

    当我们有一个接口,并且有它的实现类,那么Hilt也可以注入生成该接口。这也是Dagger的功能

    1.定义接口
    interface ICallback {
        fun onSuccess()
        fun onFailure()
    }
    
    2.实现类,并使用@Inject注解
    class CallbackImpl @Inject constructor() : ICallback {
        override fun onSuccess() {
            Log.i("aruba_log", "onSuccess")
        }
    
        override fun onFailure() {
            Log.i("aruba_log", "onFailure")
        }
    }
    
    3.定义模块抽象类,提供抽象方法返回ICallback接口

    该方法需要入参为实现类,并使用@Binds注解

    @InstallIn(ActivityComponent::class)
    @Module
    abstract class CallbackFetcher {
    
        @Binds
        abstract fun provideCallback(callback: CallbackImpl): ICallback
    }
    
    4.Activity中注入,并调用方法:
    @AndroidEntryPoint
    class ViewModelActivity : AppCompatActivity() {
        @Inject
        lateinit var callback: ICallback
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_view_model)
    
            callback.onSuccess()
        }
    }
    

    日志结果:
    I/aruba_log: onSuccess

    五、默认绑定

    Hilt定义的组件都绑定了安卓上下文相关对象,如:在ActivityComponent中注入的类,直接可以通过注入获取Activity对象
    以上面接口实现类为例子

    1.构造方法中使用@ActivityContext注解注入Context

    除了@ActivityContext,还可以通过@ApplicationContext注入全局上下文
    这边在回调时,弹一个toast

    class CallbackImpl @Inject constructor(@ActivityContext var context: Context) : ICallback {
        override fun onSuccess() {
            Log.i("aruba_log", "onSuccess")
            Toast.makeText(context, "onSuccess", Toast.LENGTH_SHORT).show()
        }
    
        override fun onFailure() {
            Log.i("aruba_log", "onFailure")
            Toast.makeText(context, "onFailure", Toast.LENGTH_SHORT).show()
        }
    }
    

    运行效果:


    2.注入Activity

    我们也可以直接定义一个Activity的成员变量,并通过构造函数注入

    class CallbackImpl @Inject constructor(var activity: Activity) : ICallback {
        override fun onSuccess() {
            Log.i("aruba_log", "onSuccess")
            Toast.makeText(activity, "onSuccess", Toast.LENGTH_SHORT).show()
        }
    
        override fun onFailure() {
            Log.i("aruba_log", "onFailure")
            Toast.makeText(activity, "onFailure", Toast.LENGTH_SHORT).show()
        }
    }
    
    3.组件-绑定对应关系

    组件下注入类可以获取对应的绑定对象

    组件 创建时机
    SingletonComponent Application
    ActivityRetainedComponent Application
    ServiceComponent Application、Service
    ActivityComponent Application、Activity
    ViewModelComponent Application
    FragmentComponent Application、Activity、Fragment
    ViewComponent Application、Activity、View
    ViewWithFragmentComponent Application、Activity、Fragment、View

    总结

    DI框架虽然带来了极大的便利,但无论是Dagger还是Hilt,在使用过程中,有必要使用流程图、UML图等来设计记录组件和作用域的绑定关系,以便后续使用与维护

    Demo地址:https://gitee.com/aruba/hilt-application.git

    相关文章

      网友评论

        本文标题:Android--Hilt入门

        本文链接:https://www.haomeiwen.com/subject/ywdgxrtx.html