Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇

简介: Hilt 是基于 Dagger 基础上进行开发的,如果了解 Dagger 朋友们,应该会感觉它们很像,但是与 Dagger 不同的是, Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,而不需要管理所有 Dagger 配置的问题。

image.png


在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记 分别介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章继续讲解 Hilt 的用法,代码已经全部上传到 GitHub:HiltWithAppStartupSimple 如果对你有帮助,请在仓库右上角帮我点个赞。


Hilt 涉及的知识点有点多而且比较难理解,在看本篇文章之前一定要先看一下之前的文章 Jetpack 新成员 Hilt 实践(一),为了节省篇幅,这篇文章将会忽略 Hilt 环境配置的过程等等之前文章已经介绍过的内容。


另外如果想了解 Google 新推出的另外两个 Jetpack 新成员 App StartupPaging3 的实践与原理,可以点击下方链接前去查看。



通过这篇文章你将学习到以下内容:


  • 什么是注解?
  • @assist 注解和 SavedStateHandle 如何使用?
  • 如何使用 @Binds 注解实现接口注入?
  • @Binds@Provides 的区别?
  • 限定符 @Qualifier 的使用?
  • 自定义限定符 @qualifers
  • 预定义的限定符 @qualifers
  • 组件作用域 @scopes 如何使用?
  • 如何在 Hilt 不支持的类中执行依赖注入?
  • Hilt 如何和 ContentProvider 一起使用?
  • Hilt 如何和 App Startup 一起使用?


Hilt 是基于 Dagger 基础上进行开发的,如果了解 Dagger 朋友们,应该会感觉它们很像,但是与 Dagger 不同的是, Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,而不需要管理所有 Dagger 配置的问题。


在上篇文章已经介绍过, Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章将介绍 Hilt 如何和 Jetpack 组件(ViewModel、App Startup)一起绑定,在开始介绍之前我们先来了解一下什么是注解。


什么是注解



之前有小伙伴在 WX 上问过我,对注解不太了解,所以想在这里想简单的提一下。


注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”,注解则可以被编译器打包进入 class 文件,可以在编译,类加载,运行时被读取。


常见的三个注解 @Override@Deprecated@SuppressWarnings


  • @Override: 确保子类重写了父类的方法,编译器会检查该方法是否正确地实现。
  • @Deprecated:表示某个类、方法已经过时,编译器会检查,如果使用了过时的方法,会给出提示。
  • @SuppressWarnings:编译器会忽略产生的警告。


Hilt 如何和 ViewModel 一起使用?



在上一篇文章只是简单的介绍了 Hilt 如何和 ViewModel 一起使用,我们继续介绍 ViewModel 的另外一个重要的参数 SavedStateHandle,首先需要添加依赖。


在 App 模块中的 build.gradle 文件中添加以下代码。


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


koltin 使用 kapt, Java 使用 annotationProcessor。


注意: 这个是在 Google 文档上没有提到的,如果使用的是 kotlin 的话需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。


// For Kotlin projects
kotlinOptions {
    jvmTarget = "1.8"
}


在 ViewModel 的构造函数中使用 @ViewModelInject 注解提供一个 ViewModel,如果需要用到 SavedStateHandle,需要使用 @assist 注解添加 SavedStateHandle 依赖项,代码如下所示。


class HiltViewModel @ViewModelInject constructor(
    private val tasksRepository: Repository,
    //SavedStateHandle 用于进程被终止时,保存和恢复数据
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    // getLiveData 方法会取得一个与 key 相关联的 MutableLiveData
    // 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。
    private val _userId: MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)
    // 对外暴露不可变的 LiveData
    val userId: LiveData<String> = _userId
    companion object {
        private val USER_KEY = "userId"
    }
}


将用户的 userId 存储在 SavedStateHandle 中,当进程被终止时保存和恢复对应的数据。


SavedStateHandle 是什么?SavedStateHandle 为了解决什么问题?


ActivityFragment 通常会在下面三种情况下被销毁(以下内容来自 Google):


  • 从当前界面永久离开: 用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish() 方法)。对应 Activity 实例被永久关闭。
  • Activity 配置 (configuration) 被改变: 例如旋转屏幕等操作,会使 Activity 需要立即重建。
  • 应用在后台时,其进程被系统杀死: 这种情况发生在设备剩余运行内存不足,系统又需要释放一些内存的时候,当进程在后台被杀死后,用户又返回该应用时 Activity 需要被重建。


ViewModel 会帮您处理第二种情况,因为在这种情况下 ViewModel 没有被销毁,而在第三种情况下,ViewModel 被销毁了, 当进程在后台被杀死后,则需要使用 onSaveInstanceState() 作为备用保存数据的方式。


SavedStateHandle 的出现就是为了解决 App 进程终止保存和恢复数据问题,ViewModel 不需要向 Activity 发送和接收状态。相反的,现在可以在 ViewModel 中处理保存和恢复数据。


SavedStateHandle 类似于一个 Bundle,它是数据的键-值映射,这个 SavedStateHandle 包含在 ViewModel 中,它在后台进程终止时仍然存在,以前保存在 onSaveInstanceState() 中的任何数据现在都可以保存在 SavedStateHandle 中。


使用 @Binds 注解实现接口注入?



注入接口实例有两种方式分别使用注解 @Binds@Provides@Provides 的方式在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记  Hilt 如何和 Room 一起使用Hilt 如何和第三方组件一起使用 都有介绍,这里我们来介绍如何使用注解 @Binds


interface WorkService {
    fun init()
}
/**
 * 注入构造函数,因为 Hilt 需要知道如何提供 WorkServiceImpl 的实例
 */
class WorkServiceImpl @Inject constructor() :
    WorkService {
    override fun init() {
        Log.e(TAG, " I am an WorkServiceImpl")
    }
}
@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ActivityComponent,因此 WorkServiceModule 绑定到 ActivityComponent 的生命周期。
abstract class WorkServiceModule {
    /**
     * @Binds 注解告诉 Hilt 需要提供接口实例时使用哪个实现
     *
     * bindAnalyticsService 函数需要为 Hilt 提供了以下信息
     *      1. 函数返回类型告诉 Hilt 提供了哪个接口的实例
     *      2. 函数参数告诉 Hilt 提供哪个实现
     */
    @Binds
    abstract fun bindAnalyticsService(
        workServiceImpl: WorkServiceImpl
    ): WorkService
}


使用注解 @Binds 时,需要提供以下两个信息:


  • 函数参数告诉 Hilt 接口的实现类,例如参数 WorkServiceImpl 是接口 WorkService 的实现类。
  • 函数返回类型告诉 Hilt 提供了哪个接口的实例。


注解 @Binds 和 注解 @Provides 的区别?


  • @Binds:需要在方法参数里面明确指明接口的实现类。
  • @Provides:不需要在方法参数里面明确指明接口的实现类,由第三方框架实现,通常用于和第三方框架进行绑定(RetrofitRoom 等等)


// 有自己的接口实现
@Binds
abstract fun bindAnalyticsService(
    workServiceImpl: WorkServiceImpl
): WorkService
// 没有自己的接口实现
@Provides
fun providePersonDao(application: Application): PersonDao {
    return  Room
        .databaseBuilder(application, AppDataBase::class.java, "dhl.db")
        .fallbackToDestructiveMigration()
        .allowMainThreadQueries()
        .build().personDao()
}
@Provides
fun provideGitHubService(): GitHubService {
    return  Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(GitHubService::class.java)
}


限定符 @Qualifier 注解的使用



来自 Google@Qualifier 是一种注解,当类型定义了多个绑定时,使用它来标识该类型的特定绑定。


换句话说 @Qualifier 声明同一个类型,可以在多处进行绑定,我将限定符分为两种。


  1. 自定义限定符
  2. 预定义限定符


自定义限定符的使用


我们先用注解 @Qualifier 声明两个不同的实现。


// 为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
@Qualifier
// @Retention 定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
@Retention(AnnotationRetention.BINARY)
annotation class RemoteTasksDataSource // 注解的名字,后面直接使用它
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalTasksDataSource


  • @Qualifier :为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
  • @Retention:定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
  • AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中。
  • AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见。
  • AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见。


通常我们自定义的注解都是 RUNTIME,所以务必要加上@Retention(RetentionPolicy.RUNTIME) 这个注解


来看一下 @Qualifier@Provides 一起使用的例子,定义了两个方法,具有相同的返回类型,但是实现不同,限定符将它们标记为两个不同的绑定。


@Singleton
@RemoteTasksDataSource
@Provides
fun provideTasksRemoteDataSource(): DataSource { // 返回值相同
    return RemoteDataSource() // 不同的实现
}
@Singleton
@LocalTasksDataSource
@Provides
fun provideTaskLocalDataSource(appDatabase: AppDataBase): DataSource { // 返回值相同
    return LocalDataSource(appDatabase.personDao()) // 不同的实现
}


当我们声明完 @Qualifier 注解之后,就可以使用声明的两个 @Qualifier,来看个例子,定义一个 Repository 构造方法里面传入用 @Qualifier 注解声明的两个不同实现。


@Singleton
@Provides
fun provideTasksRepository(
    @LocalTasksDataSource localDataSource: DataSource,
    @RemoteTasksDataSource remoteDataSource: DataSource
): Repository {
    return TasksRepository(
        localDataSource,
        remoteDataSource
    )
}


provideTasksRepository 方法内,传入的参数都是 DataSource,但是前面用 @Qualifier 注解声明了它们不同的实现。


预定义限定符


Hilt 提供了一些预定义限定符,例如你可能在不同的情况下需要不同的 ContextApplictionActivity)Hilt 提供了 @ApplicationContext@ActivityContext 两种限定符。


class HiltViewModel @ViewModelInject constructor(
    @ApplicationContext appContext: Context,
    @ActivityContext actContext: Context,
    private val tasksRepository: Repository,
    @Assisted private val savedStateHandle: SavedStateHandle
)


组件作用域 @scopes 的使用



默认情况下,Hilt 中的所有绑定都是无作用域的,这意味着每次应用程序请求绑定时,Hilt 都会创建一个所需类型的新实例。


@scopes 的作用在指定作用域范围内(ApplicationActivity 等等) 提供相同的实例。


Hilt 还允许将绑定的作用域限定到特定组件,Hilt 只为绑定作用域到的组件的每个实例创建一次范围绑定,所有绑定请求共享同一个实例,我们来看一例子。


@Singleton
class HiltSimple @Inject constructor() {
}


HiltSimple@Singleton 声明了其作用域,那么在 Application 范围内提供相同的实例,代码如下所示,大家可以运行 Demo 看一下输出结果。


MainActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417
HitAppCompatActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417


注意:绑定组件范围可能非常的昂贵,因为提供的对象会保留在内存中,直到该组件被销毁,应该尽量减少在应用程序中使用绑定组件范围,对于要求在一定范围内使用同一实例的绑定,或者对于创建成本高昂的绑定,使用组件范围的绑定是合适的。


下表列出了每个生成组件的 scope 注解对应的范围。


Android class Generated component Scope
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped


在 Hilt 不支持的类中执行依赖注入



Hilt 支持最常见的 Android 类 ApplicationActivityFragmentViewServiceBroadcastReceiver 等等,但是您可能需要在 Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。

@EntryPoint:可以使用 @EntryPoint 注解创建入口点,@EntryPoint 允许 Hilt 使用 Hilt 无法在依赖中提供依赖的对象。


例如 Hilt 不支持 ContentProvider,如果你在想在 ContentProvider 中获取 Hilt 提供的依赖,你可以定义一个接口,并添加 @EntryPoint 注解,然后添加 @InstallIn 注解指定 module 的范围,代码如下所示。


@EntryPoint
@InstallIn(ApplicationComponent::class)
interface InitializerEntryPoint {
    fun injectWorkService(): WorkService
    companion object {
        fun resolve(context: Context): InitializerEntryPoint {
            val appContext = context.applicationContext ?: throw IllegalStateException()
            return EntryPointAccessors.fromApplication(
                appContext,
                InitializerEntryPoint::class.java
            )
        }
    }
}


使用 EntryPointAccessors 提供四个静态方法进行访问,分别是 fromActivityfromApplicationfromFragmentfromView 等等


image.png


EntryPointAccessors 提供四个静态方法,第一个参数是 @EntryPoint 接口上 @InstallIn 注解指定 module 的范围,我们在接口 InitializerEntryPoint@InstallIn 注解指定 module 的范围是 ApplicationComponent,所以我们应该使用 EntryPointAccessors 提供的静态方法 fromApplication


class WorkContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        context?.run {
            val service = InitializerEntryPoint.resolve(this).injectWorkService()
            Log.e(TAG, "WorkContentProvider ${service.init()}")
        }
        return true
    }
    ......
}


ContentProvider 中调用 EntryPointAccessors 类中的 fromApplication 方法就可以获取到 Hit 提供的依赖。


Hilt 如何和 App Startup 一起使用


App Startup 会默认提供一个 InitializationProviderInitializationProvider 继承 ContentProvider,那么 Hilt 在 App Startup 中使用的方式和 ContentProvider 一样。


class AppInitializer : Initializer<Unit> {
    override fun create(context: Context): Unit {
        val service = InitializerEntryPoint.resolve(context).injectWorkService()
        Log.e(TAG, "AppInitializer ${service.init()}")
        return Unit
    }
    override fun dependencies(): MutableList<Class<out Initializer<*>>> =
        mutableListOf()
}


通过调用 EntryPointAccessors 的静态方法,获取到 Hit 提供的依赖,关于 App Startup 如何使用可以查看这篇文章 Jetpack 最新成员 AndroidX App Startup 实践以及原理分析


总结



到这里关于 Hilt 的注解使用都介绍完了,代码已经全部上传到了 GitHub:HiltWithAppStartupSimple


HiltWithAppStartupSimple 包含了本篇文章和 Jetpack 新成员 Hilt 实践(一)启程过坑记 文章中使用的案例,如果之前没有看过可以先去了解一下,之后看代码会更加的清楚。


Hilt 是基于 Dagger 基础上进行开发的,入门要比 Dagger 简单很多,不需要去管理所有的 Dagger 的配置问题,但是其入门的门槛还是很高的,尤其是 Hilt 的注解,需要了解其每个注解的含义才能正确的使用,避免资源的浪费。


这篇文章和之前 Jetpack 新成员 Hilt 实践(一)启程过坑记 的文章其中很多案例都重新去设计了,因为 Google 的提供的案例,确实很难让人理解,希望这两篇文章可以帮助小伙伴们快速入门 Hilt,后面还会有更多实战案例。


计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请在仓库右上角帮我点个赞,后面我会陆续完成更多 Jetpack 新成员的项目实践。


结语



致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。


算法


由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。


  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……


每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。


Android 10 源码系列


正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。



Android 应用系列



精选译文


目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。



工具系列




目录
相关文章
|
10天前
|
数据采集 网络协议 算法
移动端弱网优化专题(十四):携程APP移动网络优化实践(弱网识别篇)
本文从方案设计、代码开发到技术落地,详尽的分享了携程在移动端弱网识别方面的实践经验,如果你也有类似需求,这篇文章会是一个不错的实操指南。
30 1
|
1月前
|
测试技术 数据库 Android开发
深入解析Android架构组件——Jetpack的使用与实践
本文旨在探讨谷歌推出的Android架构组件——Jetpack,在现代Android开发中的应用。Jetpack作为一系列库和工具的集合,旨在帮助开发者更轻松地编写出健壮、可维护且性能优异的应用。通过详细解析各个组件如Lifecycle、ViewModel、LiveData等,我们将了解其原理和使用场景,并结合实例展示如何在实际项目中应用这些组件,提升开发效率和应用质量。
44 6
|
2月前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
77 8
|
3月前
|
XML 安全 Java
App安全检测实践基础——工具
App安全检测实践基础——工具
94 0
|
5月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
5月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
5月前
|
存储 Android开发 Kotlin
开发安卓app OKhttp下载后使用MediaPlayer播放
在Android Jetpack Compose应用程序中,要使用OkHttp下载远程音频文件并在本地播放,你需要完成以下几个步骤: 1. **添加依赖**:确保`build.gradle`文件包含OkHttp和Jetpack Compose的相关依赖。 2. **下载逻辑**:创建一个`suspend`函数,使用OkHttp发起网络请求下载音频文件到本地。 3. **播放逻辑**:利用`MediaPlayer`管理音频播放状态。 4. **Compose UI**:构建用户界面,包含下载和播放音频的按钮。
|
5月前
|
存储 Android开发
安卓app,MediaPlayer播放本地音频 | 按钮控制播放和停止
在Jetpack Compose中,不直接操作原生Android组件如`Button`和`MediaPlayer`,而是使用Compose UI构建器定义界面并结合ViewModel管理音频播放逻辑。以下示例展示如何播放本地音频并用按钮控制播放/停止:创建一个`AudioPlayerViewModel`管理`MediaPlayer`实例和播放状态,然后在Compose UI中使用`Button`根据`isPlaying`状态控制播放。记得在`MainActivity`设置Compose UI,并处理相关依赖和权限。
|
6月前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
6月前
|
存储 设计模式 前端开发
构建高效安卓应用:Jetpack MVVM 架构的实践之路
【4月更文挑战第9天】 在移动开发的迅猛浪潮中,Android 平台以其开放性和灵活性受到开发者青睐。然而,随着应用复杂度的不断增加,传统的开发模式已难以满足快速迭代和高质量代码的双重要求。本文将深入探讨 Jetpack MVVM 架构模式在 Android 开发中的应用实践,揭示如何通过组件化和架构设计原则提升应用性能,实现数据驱动和UI分离,进而提高代码可维护性与测试性。我们将从理论出发,结合具体案例,逐步展开对 Jetpack MVVM 架构的全面剖析,为开发者提供一条清晰、高效的技术实施路径。

热门文章

最新文章