深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
下面获取 `Repository` 的 `LiveData` 对象,
class MainViewModel(countReserved: Int) : Int{
…
fun getUser(userId: String) : LiveData {
return Repository.getUser(userId)
}
}
在 `Activity` 中观察 `LiveData`
viewModel.getUser(userId).observe(this) { ->
}
以上的这种呢做法完全错误,因为每次调用 `getUser()` 方法返回的都是一个新的 `LiveData` 实例,而上述写法会一直观察老的 `LiveData` 实例,这种情况下,`LiveData` 是不可能观察的
以下是正确做法,
借助 `switchMap` ,它的使用场景比较固定:如果 `VIewModel` 中的某个 `LiveData` 对象是调用另外的方法获取的,那么就可以借助 `switchMap()` 方法,将这里 `LiveData` 对象转换成另外一个可观察的 `LiveData` 对象。修改 `MainViewModel` 中的代码,如下:
class MainViewModel(countReserved: Int) : ViewModel() {
…
private val userIdLiveData = MutableLiveData()
val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
`switchMap()` 方法接收两个参数:
* 第一个:传入新增的 `userIdLiveData` ,`switchMap()` 方法会对它进行观察
* 第二个:是一个转换函数,我们必须在这个转换函数中返回一个 `LiveData` 对象,因为 `switchMap()` 方法的工作原理就是要将转换函数中返回的 `LiveData` 对象转换成另一个可观察的 `LiveData` 对象。
现在 `user` 对象就是一个可观察的 `LiveData` 对象了
修改 `Activity` 中的代码,如下:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
btn_getUser.setOnClickListener {
val userId = (0…1000).random().toString()
viewModel.getUser(userId)
}
viewModel.user.observe(this,{
tv_info.text = it.firstName
})
}
}
现在已经实现了功能并且可以正常运行了。
当我们的 `ViewModel` 中某个获取数据的方法有可能是没有参数的,这个时候该怎么办呢?
这里我们先创建一个空的 `LiveData` 对象:
class MyViewModel : ViewModel(){
private val refreshLiveData = MutableLiveData<Any?>()
val refreshResult = Transformatinos.switchMap(refreshLiveData) {
Repository.refresh()
}
fun refresh() {
refreshLiveData.value = refreshLiveData.value
}
}
在 `refresh()` 方法中,只是将 `refreshLiveData` 原有的数据取出来(默认是空),再重新设置到 `refreshResult` 当中,这样就能触发一次数据变化。
在 `LiveData` 内部不会判断即将设置的数据和原有数据是否相同,只要调用了 `setValue()` 或 `postValue()` 方法,就一定会触发数据变化事件,然后我们只需要在 `Activity` 中观察 `refreshResult` 这个 `LiveData` 对象即可
## Room
`Room` 是 Google 官方推出的一个 `ORM`(`Object Relational Mapping` 对象关系映射)框架,并将它加入了 `Jetpack` 中。
`Room` 的整体结构主要由 `Entity`、`Dao`、`Database` 这三个部分组成,每个部分都有自己明确的职责:
* `Entity`:用于定义封装实际数据的实体类,每个实体类都会在数据库中都有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
* `Dao`:`Dao` 是数据访问的意思,同长会在这里对数据库的各项操作进行封装。在实际编程中,逻辑层就不用和底层数据打交道了,直接和 `Dao` 层进行交互就可。
* `Database`:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供 `Dao` 层的访问实例。
### Room 的具体用法
添加依赖:
…
apply plugin: ‘kotlin-kapt’
dependencies{
…
implementation ‘androidx.room:room-runtime:2.2.5’
kapt ‘androidx.room:room-compiler:2.2.5’
}
`kapt` 只能在 `Kotlin` 项目中使用,如果是 `Java` 项目的话,使用 `amotationProcessor` 即可
接下来按照刚才介绍的 `Room` 的三个部分来一一进行实现
#### 定义 Entity:
我们直接就使用 上文定义的 User 类来改造
@Entity
data class User(var firstName: String,var lastName:String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0 //给每个实体类都添加一个字段,并设为主键
}
`@Entity` 注解:将 `User` 声明成了一个实体类
`@PrimaryKey` 注解:将 `id` 字段设为主键,将 `autoGenerate` 设置为 `true` ,使得主键的值自动生成
#### 定义 Dao:
这一部分比较关键,因为所有访问数据库的操作都是在这里封装
新建一个 UserDao 接口,如下:
@Dao
interface UserDao {
@Insert
fun insetUser(user: User) : Long
@Update
fun updateUser(newUser: User)
@Query("select * from User")
fun loadAllUser() : List<User>
@Query("select * from User where age > :age")
fun loadUsersOlderThan(age: Int) : List<User>
@Delete
fun deleteUser(user: User)
@Query("delete from User where lastName = :lastName")
fun deleteUserByLastName(lastName: String) : Int
}
`@Dao` 注解:让 `Room` 能够识别到 `UserDao` 是一个 `Dao`
`@Insert` 注解:表示将参数传入 `User` 对象插入数据库中,插入完成后还会返回自动生成的主键 `id` 值
`@Update` 注解:表示会将参数中传入的 `User` 对象更新到数据库当中
`@Delete` 注解:表示会将参数传入的 `User` 对象从数据库中删除
以上几种数据库操作都直接使用注解表示即可,不用编写 `SQL` 语句。如果想要从数据库中查询数据,或者使用非实体类参数来增删改查数据,就必须编写 `SQL` 语句,比如刚刚定义的 `loadAllUsers()` 方法,用于从数据库中查询所有用户。
`@Query` 注解:该注解中必须编写 `SQL` 语句,可以将方法中传入的参数指定到 `SQL` 语句当中,比如 `loadUserOlderThan()` 方法就可以查询所有年龄大于指定参数的用户。
如果是使用非实体类参数来增删改查数据,也要编写 `SQL` 语句才行,而且只能使用 `@Query` 注解,比如 `deleteUserByLastName()` 方法
#### 定义 Database
这部分一般只需要定义三个部分:数据库版本号、包含哪些实体类、提供 `Dao` 层的访问实例。
新建一个 `AppDatabase.kt` 文件,如下:
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
companion object{
private var instance: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.build().apply {
instance = this
}
}
}
}
`@Database` 注解:其中声明了数据库版本号以及包含的实体类,多个实体类之间用逗号隔开。
`AppDatabase` 类必须继承自 `RoomDatabase` 类,并且一定要使用 `abstract` 关键字声明成抽象类,然后提供相应的抽象方法,用于获取之前的 `Dao` 实例,比如这里提供的 `userDao()` 方法。之后在 `companion object` 结构体中编写了一个单例模式,原则上全局应该只存在一个 `Appdatabase` 实例。
`databaseBuilder()` 方法接收三个参数:
* 第一个:参数一定要使用 `applicationContext` ,而不能使用普通 `context()` ,否则容易出现内存泄漏的情况。
* 第二个:参数是 `AppDatabase` 的 `Class` 类型。
* 第三个:参数是数据库名
修改 `MainActivity` 中的代码:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User(“Tony”,“ll”,11)
val user2 = User(“Tom”,“SS”,22)
btn_addData.setOnClickListener {
thread {
user1.id = userDao.insetUser(user1)
user2.id = userDao.insetUser(user2)
}
}
btn_updateData.setOnClickListener {
thread {
user1.age = 33
userDao.updateUser(user1)
}
}
btn_deleteData.setOnClickListener {
thread {
userDao.deleteUserByLastName(“SS”)
}
}
btn_queryData.setOnClickListener {
thread {
for (user in userDao.loadAllUser()) {
Log.d(“MainActivity”,user.toString())
}
}
}
}
}
在 `AddData` 的点击事件中将 `insertUser()` 方法返回的主键 `id` 值赋值给了原来的 `User` 对象。之所以这样是因为使用 `@Update`、`@Delete` 注解去更新和删除数据时都是基于这个 `id` 值来操作的。
由于数据库操作属于耗时操作,`Room` 默认不允许在主线程中进行数据库操作,因此上述的增删改查操作都放在了子线程中,不过为了方便调试,`Room` 还提供了一个更加简单的方法:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, “app_database”)
.allowMainThreadQueries()
.build()
这样 `Room` 就允许在主线程进行数据库操作了,**不过建议只在测试环境使用**
#### Room 的数据库升级
`Room` 在数据库升级方面设计得比价繁琐,并没有比原生的 `SQLiteDatabase` 简单到哪儿去。如果你目前还只是在开发测试阶段,不想编写那么繁琐的数据库升级逻辑,`Room` 提供了一个简单粗暴的方法:
Room.databaseBuilder(aontext.applicationContext, AppDatabase::class.java. “app_database”)
.fallbackToDestructiveMigration()
.build()
构建 `AppDatabase` 实例时加入了一个 `fallbackToDestructiveMigration()` 方法,这样只要数据库进行了升级,`Room` 就会将当前的数据库销毁,然后再重新创建,但这样做的问题也就显而易见,之前数据库中的数据也会全部丢失。
下面是正规用法
我们先新建一个 Book 的实体类:
@Entity
data class Book(var nane:String, var price: Int) {
@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
然后创建一个 BookDao 接口,并在其中随意定义一些 API
@Dao
interface BookDao {
@Insert
fun insert(book: Book) : Long
@Query("select * from Book")
fun loadAllBooks() : List<Book>
}
修改 AppDatabase 中的代码:
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
abstract fun bookDao() : Book
companion object{
val MIGRATION_1_2 = object : Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, price integer not null")
}
}
private var instance: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2)
.build().apply {
instance = this
}
}
}
}
我们在 `@Database` 注释中将版本号升级到了 **2** ,并将 `Book` 嘞添加到了实体类声明中。
在`companion object` 结构体中,实现了一个 `Migration` 的匿名类,并传入了 **1** 和 **2** 这两个参数,表示当前数据库版本从 **1** 升级到 **2** 的时候就执行这个匿名类中的升级逻辑。这里是要新增一张 `Book` 表,所以需要在 `migrate()` 方法中编写相应的建表语句。`Book` 表的建表语句必须和 `Book` 实体类中声明的结构完全一致。最后在构建 `AppDatabase` 实例时,加入一个 `addMigrations()` 方法,并将参数传入即可。
当我们的数据库可能不需要新建表,而是添加一个列时,可以使用 `alter` 语句修改表结构即可,如下:
@Entity
data class Book(var nane:String, var price: Int, var pages: Int) {
@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
这里添加了一个页数 `pages` 的字段,之后修改 `AppDatabase` 的代码:
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
…
companion object{
…
var MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(“alter table Book add column pages integer not null default ‘unknown’”)
}
}
private var instance: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
....
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2,MIGRATION_2_3)
.build().apply {
instance = this
}
}
}
}
升级步骤和之前差不多,就不多说了
## WorkManager
`WorkManager` 适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用 `AlarmManager` 实现还是 `JobScheduler` 实现,而且它还支持周期性任务、链式任务处理等功能。
`WorkManager` 的基本用法主要分以下三步:
1. 定义一个后台任务,并实现具体的任务逻辑;
2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
3. 将改后台任务请求传入 `WorkManager` 的 `enqueue()` 方法中,系统会在合适的时间运行。
下面来按照以上步骤来实现。第一步,定义一个后台任务,新建一个 `SimpleWorker` 类,如下:
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context,params) {
override fun doWork(): Result {
Log.d(“SimpleWorker”, “do work in SimpleWorker”)
return Result.success()
}
}
每一个后台任务都必须继承 `Work` 类。`doWork()` 方法不会运行在主线程中,因此可以在这里执行耗时操作,另外该方法要求返回一个 `Result` 对象,用于表示任务的运行结果。成功就是 `Result.success()` ,失败是 `Result.failure()` 。除了这两种还有一个 `Result.retry()` 方法,它其实也代表着失败,只是可以结合 `WorkRequest.Builder` 的 `setBackoffCriteria()` 方法来重新执行任务。
第二步,配置该后台任务的运行条件和约束信息。
这里进行最基本的配置,代码如下:
var request= OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
`OneTimeWorkRequest.Builder` 是 `WorkRequest.Builder` 的子类,用于构建单词运行的后台任务请求,还有另一个 `PeriodicWorkRequest.Builder` 也是其子类,用于构建周期性运行的任务请求,但为了降低设备性能消耗,它的构造函数中传入的运行周期间隔不能短于 15 分钟,实例如下:
var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15,
TimeUnit.MINUTES).build()
第三步,最后只需要将构建好的后台任务请求传入 `WorkManager` 的 `enqueue()` 方法中,系统就会在合适的时间去运行了:
WorkManager.getInstance(context).enqueue(request)
下面来测试下,修改 `MainActivity` 中的代码:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
btn_doWork.setOnClickListener {
val request= OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
WorkManager.getInstance(this).enqueue(request)
}
}
}
这里后台任务的具体时间由我们所指定的约束以及系统的一些优化所决定的,由于这里没有指定任何约束,因此后台任务基本上会在点击按钮之后立刻运行。
### 使用 WorkManager 处理复杂任务
让后台任务在指定的延迟时间后运行,可以借助 `setInitialDelay()` 方法,如下:
var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
`setInitalDelay()` 接收两个参数,第一个是要延迟的时间,第二个是该时间的单位:可以选择毫秒、秒、分钟、小时、天都可以。
可以控制运行时间之后,再来添加点别的功能,比如给后台任务请求添加标签,如下所示:
var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java)
…
.addTag(“simple”)
.build()
该功能最主要的一个功能就是通过标签来取消后台任务请求:
WorkManager.getInstance(this).cancelAllWorkByTag(“simple”)
使用标签可以将可以将同一标签名的所有后台请求全部取消
如果没有标签,也可以通过 id 来来取消后台任务请求:
WorkManager.getInstance(this).cancelAllWorkById(request.id)
如果想要一次性取消所有后台任务:
WorkManager.getInstance(this).cancelAllWork()
在上文中提到,如果 `doWork()` 方法中返回了 `Result.retry()`,那么可以结合 `setBackoffCriteria()` 方法来重新执行任务,如下:
var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java)
…
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()
`setBackoffCriteria()` 接收三个参数:
* 第一个:参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。该参数可选值有两种:
+ LINEAR:表示下次重试时间以线性的方式延迟。
+ EXPONENTIAL:表示下次重试的时间以指数的方式延迟。
* 第二个和第三个都用于指定在多久之后重新执行任务,时间最短不能少于 10 秒钟
接下来我们也可以对 `Result.success(`) 和 `Result.failure()` 任务结果进行监听,如下:
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(request.id)
.observe(this) { workInfo ->
if(workInfo.state == WorkInfo.State.SUCCEEDED) {
Log.d(“MainActivity”, “do work succeeded”)
} else if(workInfo.state == WorkInfo.State.FAILED) {
Log.d(“MainActivity”. “do work failed”)
}
}
这里调用了 `getWorkInfoByIdLiveData()` 方法,并传入后台任务请求的 `id` ,会返回一个 `LiveData` 对象,接着就可以调用 `LiveData` 对象的 `observe()` 方法来观察数据变化了,以此监听后台任务的运行结果。
另外,调用 `getWorkInfoByTagLiveData()` 方法也可以监听同一标签名下所有后台任务请求的运行结果,用法差不多。
### 链式任务
假设定义了 3 个独立的后台任务:同步数据、压缩数据、上传数据,现在要实现先同步、再压缩、对吼上传的功能,就可以借助链式任务来实现,如下:
val sync = …
val compress = …
val upload = …
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()
`beginWith()` 方法用于开启一个链式任务,后面要执行的任务只需要使用 `then()` 方法来连接即可。
另外,`WorkManager` 还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行,也就是说,如果某一个任务运行失败、或者取消了,之后的任务就都不能运行了
以上介绍的 `WorkManager` 的所有功能,在国产手机上都有可能得不到正确的运行,因为绝大多数的国产手机厂商在进行 `Android` 系统定制时会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序,而被杀死的应用程序既无法接收广播,也无法运行 `WorkManager` 的后台任务,所有千万别依赖 `WorkManager` 去实现什么核心功能,因为它在国产手机上可能会非常不稳定
#### [JetPack全家桶系列:`https://qr18.cn/A0gajp`]( )
![img](https://img-blog.csdnimg.cn/img_convert/a67a85197e6664f7afcf0de82708d8dd.png)
![img](https://img-blog.csdnimg.cn/img_convert/b2e1921f3e1ac96497c41116fa7a0955.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
接即可。
另外,`WorkManager` 还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行,也就是说,如果某一个任务运行失败、或者取消了,之后的任务就都不能运行了
以上介绍的 `WorkManager` 的所有功能,在国产手机上都有可能得不到正确的运行,因为绝大多数的国产手机厂商在进行 `Android` 系统定制时会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序,而被杀死的应用程序既无法接收广播,也无法运行 `WorkManager` 的后台任务,所有千万别依赖 `WorkManager` 去实现什么核心功能,因为它在国产手机上可能会非常不稳定
#### [JetPack全家桶系列:`https://qr18.cn/A0gajp`]( )
[外链图片转存中...(img-q6lp6Uuh-1715642603478)]
[外链图片转存中...(img-uSIO6exj-1715642603478)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**