Jetpack(二)

LiveData的map() , switchMap()

比如一个user类,类里面包含了一个名字和年龄,可是我只想要user里面的年龄,这时候怎么办呢,这时就得用map()或者switchMap()了。

究其根本它的作用就是:把一个类型变成另一个类型

map()

如果希望在将 LiveData 对象分派给观察者之前对存储在其中的值进行更改,或者需要根据另一个实例的值返回不同的 LiveData 实例,可以使用LiveData中提供的Transformations类。(这句话没太看懂。。。)

public class MainViewModel extends ViewModel {

        //Integer类型的liveData1
        MutableLiveData<Integer> liveData1 = new MutableLiveData<>();
        TextView textView = findViewById(R.id.textview);
        //转换成String类型的liveDataMap
        //Transformations类的map方法接受两个参数,第一个是要转化的livedata对象,第二个是转换函数
        LiveData<String> liveDataMap = Transformations.map(liveData1, new Function<Integer, String>() {
            @Override
            public String apply(Integer input) {
                String s = input + "好好听课";
                Log.i("TAG", "apply: " + s);
                return s;
            }
        });
        liveDataMap.observe(this, new Observer<String>() {
            @Override
            public void onChanged(String s) {
                Log.i("TAG", "onChanged1: "+s);
                textView.setText(s);
            }
        });
        liveData1.setValue(100);
    }

switchMap()

他的使用场景非常固定,但是可能比map()更加常用 。

前面我们所学的内容都有一个前提,LiveData对象的实例都是在ViewModel中创造的。然后在实际中,很有可能 ViewModel中的某个LiveData对象是调用另外的方法获取的。下面我们来模拟一下这种情况,新建一个Repository单例类:

object Repository{
    fun getUser(userId:String): LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value = User(userId,userId,0)
        return liveData
    }
}

按照正常逻辑,我们应该根据传入的userId去服务器请求或者到数据库中查找相应的User对象,但这里只是模拟示例,因此每次将传入的userId当作用户姓名来创建一个新的User对象即可。需要注意的是,getUser返回的是一个包含User数据的LivaData对象,并且每次调用getUser方法都会返回一个新的 LivaData实例。

然后我们在MainViewModel中也定义一个getUser方法:

class MainViewModel(countReserved: Int) : ViewModel(){
    ...
    fun getUser(userId:String): LiveData<User>{
        return Repository.getUser(userId)
    }
}

接下来的问题是我们如何在Activity中观察LiveData的数据变化呢?既然getUser返回的就是一个LiveData对象,那么可以这样写吗?

viewModel.getUser(userId).observe(this){ user ->

}

请注意,这么做是完全错误的。因为每次调用getUser返回的是一个新的LivaData对象,而上述写法会一直观察旧的LivaData对象,从而根本无法观察到数据的变化,这种情况下的LivaData是不可观察的。

借助 switchMap()可以解决这一点,他的使用场景非常固定–ViewModel中的某个LiveData对象是调用另外的方法获取的 借助switchMap(),我们可以将这个LiveData对象转化成另一个可观察的LiveData对象。修改MainViewModel:

class MainViewModel(countReserved: Int) : ViewModel(){
    ...
    private val userIdLiveData = MutableLiveData<String>()
    val user:LiveData<User> = Transformations.switchMap(userIdLiveData){ userId ->
         Repository.getUser(userId)                                                               
    }
    fun getUser(UserId: String){
        userIdLiveData.value = userId
    }
}

这里我们定义了一个新的userIdLiveData对象,用来观察userId的数据变化,然后调用Transformations的switchMap方法,用来对另一个可观察的LiveData进行转化。switchMap方法接受两个参数:第一个参数传入我们新增的userIdLiveData,switchMap方法会对他进行观察 :第二个参数是一个转化函数,我们必须在这个转化函数中返回一个LiveData对象,因为 switchMap的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象。那么我们只要在转换函数中调用Repository.getUser(userId)即可。

switchMap方法的工作流程:

当外部调用MainViewModel的 getUser方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到 userIdLiveData中。一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap方法就会执行,并且调用我们编写的转换函数。在转换函数中调用Repository.getUser方法获取真正的用户数据。同时,switchMap方法会将Repository.getUser方法返回的LiveData对象转换成一个可观察的LiveData对象。对于activity而言,只要去观察这个LiveData对象就可以了。

最后,在刚在我们调用MainViewModel的getUser时传入了一个userId参数,为了观察这个参数的数据变化,又构建了一个userIdLiveData,然后再switchMap方法会对他进行观察就可以了。但是ViewModel中获取某个数据的方法有可能时没有参数的,这时候该怎么办呢?

class MyViewModel : ViewModel(){
    private val refreshLiveData = MutableLiveData<Any?>()//在没有可观察数据的情况下我们需要创建一个空的LiveData对象
    val refreshResult = Transformations.switchMap(userIdLiveData){
         Repository.refresh()                                                               
    }
    fun refresh(){
        refreshLiveData.value = refreshLiveData.value
    }
}

注意非常重要的一步 :我们在refresh方法中,将refreshLiveData原有的数据取出来 然后重新设置给refreshLiveData,这样就能触发一次数据变化。

LiveData内部不会判断即将设置的数据和原有数据是否相同,只要调用了setValue() 或postValue()就一定会触发数据变化事件。

小结

目前我们似乎只看到LiveData和ViewModel结合在一起使用,并没有LIfecycles什么事儿。并非如此,LiveData之所以能够成为Activity和ViewModel之间沟通的桥梁,并且不会有内存泄漏的风险,靠的就是 LIfecycles。LiveData在内部使用了 LIfecycles来自动感知生命周期的变化,从而可以在Activity销毁的时候及时释放引用,避免产生内存泄漏的问题。另外,由于要减少性能消耗,当活动处于不可见状态时(比如手机息屏,或者被其他活动遮挡),LiveData数据如果发生变化是不会通知给观察者的,只有当活动重新恢复可见状态时才会通知,这些都是LIfecycles的作用。

Room

之前我们学习了SQLite数据库的使用方法,不过当时仅仅是使用了一些原生的API来进行数据的增删改查。这些原生API虽然简单易用,但如果放到大型项目中会让代码变得混乱。

ORM(Object Relational Mapping)–对象关系映射。我们使用的编程语言是一种面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM。它让我们可以使用面向对象的思维来和数据库进行交互,绝大多数情况下不用在和SQL语句打交道了。Android官方推出了一个ORM框架,并把它加入了Jetpack当中,这就是Room。

使用Room进行增删改查(以kotlin为主)

🎈 Room的整体结构:

**🔲Entity(个体):**用于定义封装实际数据的实体类。每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。

🔲**Dao(data access object–数据访问对象)😗*一个用于与数据库进行交互的接口或抽象类。通常会在这里对数据库的各项数据进行封装,逻辑层就不需要和底层数据打交道了,直接和Dao进行交互即可。

**🔲Database:**用于定义数据库中的关键信息。包括数据库的版本号、包括的实体类以及提供Dao层的访问实例。

1.Room的依赖

请注意:2.4.3版本以后都使用kotlin,如果要使用Java是仙女,只能把版本降到2.4.3才能使用@Entity、@Dao等注释

apply plugin: 'kotlin-kapt'
dependencies {
//   def room_version = "2.5.1"
    def room_version = "2.4.3"

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
	
    //implementation "androidx.room:room-runtime:2.1.0"
    //kapt "androidx.room:room-compiler:2.1.0"    
}

新增了一个kotlin-kapt插件,同时添加了两个Room的依赖库。Room会根据项目中声明的注释来动态生成代码,因此一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能要添加插件。

2.Entity

一个良好的数据库编程建议是给每个实体类都添加一个id字段,并将这个字段设为主键。主键(Primary Key)是一个或多个用于唯一标识数据库表中每一行的字段。主键确保了表中每行数据的唯一性,并且可以防止数据出现重复。主键不允许为空(NULL),并且通常在数据库表中作为第一个字段出现。delete和update数据都是基于id值来操作的。

@Entity
data class User(var firstName: String, var lastName: String, var age: Int){
    @PrimaryKey(autoGenerate = true)  //表示自动生成主键
    var id: Long = 0
}

3.Dao

所有数据库访问的操作都是在这里封装的。

@Dao
interface UserDao{//注意:是一个接口!!!
    @Insert
    fun insertUser(user: User): Long  //插入后将自动生成的id值返回
    
    @Update
    fun updateUser(newUser: User)
       
    @Delete
    fun deleteUser(user: User)      //不用编写SQL语句
    
    @Query("select * from User")
    fun loadAllUsers(): List<User>
    
    @Query("select * from User where age > :age")
    fun loadUsersOlderThan(age: Int): List<User>
    
    @Query("delete from User where lastName = : lastName") //使用非实体类参数来增删改数据
    fun deleteUserByLastName (lastName: String): Int
}

如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。

Room将无法知道我们想要查询哪些数据,因此必须在@Query注解中编写具体的SQL语句。我们还可以将方法中传入的参数指定到SQL语句中。如果是使用非实体类参数来增删改数据,那么也要编写SQL语句才行,这个时候不能使用@Insert、@Update、 @Delete注解,而是都要使用 @Query注解才行。

Room支持编译时动态检查SQL语句语法。

4.Database

这部分写法非常固定:数据库的版本号、包含的实体类、提供Dao层的访问实例

@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase(){
    
    abstract fun userDao(): UserDao  //获取Dao实体类
    
    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
                    }
        }
    }
}

注意:1.抽象类 2.继承RoomDatabase 3.注解中声明包含的实体类以及版本号,多个实体类之间用逗号隔开 4.提供相应的抽象方法,用于获取之前编写的Dao的实例,我们只需要进行方法声明即可,具体的方法实现由Room在底层自动完成。5.在companion object中编写了一个单例模式,因为原则上全局只存在一份AppDatabase的实例。这里使用instance变量来缓存AppDatabase的实例,然后在getDatabase中判断:如果不为空直接返回,如果为空就用 Room.databaseBuilder构建一个AppDatabase的实例。该方法接受三个参数,第一个参数一定要使用applicationContext而不能使用普通的context,第三个参数是数据库名。

Room的数据库升级

现在我们打算在数据库中添加一张Book表,先创建一个Book的实体类:

@Entity
data class Book(var name: String, var pages: Int){
    @PrimaryKey(autoGenerate = true)  
    var id: Long = 0
}
@Dao
interface BookDao{
    @Insert
    fun insertBook(book: Book): Long
    
    @Query("select * from Book")
    fun loadAllBooks():List<Book>
}
@Database(version = 2, entities = [User::class, BOok::class])
abstract class AppDatabase : RoomDatabase(){
    
    abstract fun userDao(): UserDao  
    
    abstract fun bookDao(): BookDao
    
    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,
                                pages integer not null)")
            }
        }
    }
    
    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")
            		.addMigrations(MIGRATION_1_2)
            		.build().apply{
                        instance = this
                    }
        }
    }
}

注意:1.注解中我们将版本号升级到了2,并将Book类添加到了实体类生命中,然后提供了 bookDao()方法用于获取BookDao的实例 2.在companion object结构体中,我们实现了Migration的匿名类,并传入了1和2两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑,匿名类实例的变量命名命名成MIGRATION_1_2,可读性更高。 3.在migrate()方法中编写相应的建表语句,建表语句必须和Book实体类中声明的结构完全一致,否则会抛出异常。 4.在构建AppDatabase实例的时候,,加入一个addMigrations方法,并把MIGRATION_1_2传入。


不过升级数据库并不一定要新建一张表,也有可能是向现有的表中添加新的列,这种情况只需要使用alter语句修改表结构就可以了。

比如我们现在要在Book表中新增一个作者字段。

@Entitydata class Book(var name: String, var pages: Int, var author: String){   
    @PrimaryKey(autoGenerate = true)      
    var id: Long = 0
}
@Database(version = 3, entities = [User::class, BOok::class]) //new
abstract class AppDatabase : RoomDatabase(){
    
    abstract fun userDao(): UserDao  
    
    abstract fun bookDao(): BookDao
    
    companion object{
        val MIGRATION_2_3 = object : Migration(2,3){
            override fun migrate(database: SupportSQLiteDatabase){
                database.execSQL("create table Book add column author text not null default 'unknown' ") //new
            }
        }
    }
    
    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")
            		.addMigrations(MIGRATION_1_2,MIGRATION_2_3)//new 
            		.build().apply{
                        instance = this
                    }
        }
    }
}

WorkManager

Android的后台机制是一个很复杂的问题。在很早事前Android系统的后台功能是非常开放的,Service的优先级也很高,仅次于Activity,那个时候可以在Service中做很多事情,但由于后台的功能太过于开放,每个应用都想无限占用后台资源,导致手机内存越来越紧张,耗电越来越快,也变得越来越卡。为了解决这些情况,基本上Android系统发布一个新版本,后台权限都会被进一步收紧。

后台相关的API变更大概有这些:从4.4系统开始AlarmManager的触发时间由原来的精准变为不精准,5.0系统中加入了JobScheduler来处理后台任务,6.0系统中引入了Doze和App Standby模式用于降低手机被后台唤醒的频率,从8.0系统开始直接引用了Service的后台功能,只允许使用前台Service…

这么频繁的功能和API变更,开发者到底该如何编写后台代码才能保证应用程序在不同系统版本上的兼容性呢。为了解决这个问题,Google推出了WorkManager组件。WorkManager很适合用于一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager还是JobScheduler,另外他还支持周期性任务,链式任务处理等功能。

注意:WorkManager和Service并不相同,也没有直接联系!Service是四大组件之一,他在没有被销毁的情况下是一直保持在后台运行的。而WorkManager只是一个处理定时任务的工具,它可以保证即使是应用在退出甚至手机重启的情况下,之前注册的任务仍然会得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性的同步数据等等。

但是使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度减少cpu被唤醒的次数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值