Jetpack
以上是MVVM架构,Jetpack中的许多架构组件是专门为MVVM架构量身打造的,这一章主要介绍ViewModel、LiveData、Room。
简介 Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。
ViewModel ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中。
ViewModel还有一个非常重要的特性,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。
基本用法 通常来讲,比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel。
需要添加依赖。
1 2 3 4 dependencies { ... implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" }
绝对不可以 直接去创建ViewModel的实例,而是一定要通过ViewModelProvider来获取ViewModel的实例。
1 ViewModelProvider(<你的Activity或Fragment实例>).get (<你的ViewModel>::class .java)
之所以要这么写,是因为ViewModel有其独立的生命周期,并且其生命周期要长于Activity。如果我们在onCreate()方法中创建ViewModel的实例,那么每次onCreate()方法执行的时候,ViewModel都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MainViewModel : ViewModel () { var counter = 0 } class MainActivity : AppCompatActivity () { lateinit var viewModel: MainViewModel override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProvider(this ).get (MainViewModel::class .java) plusOneBtn.setOnClickListener { viewModel.counter++ refreshCounter() } refreshCounter() } private fun refreshCounter () { infoText.text = viewModel.counter.toString() } }
传递构造参数 解决在构造函数中传递参数的问题,借助ViewModelProvider.Factory实现,必须实现create方法。
1 2 3 4 5 6 7 8 9 10 11 class MainViewModel (countReserved: Int ) : ViewModel() { var counter = countReserved } class MainViewModelFactory (private val countReserved: Int ) : ViewModelProvider.Factory { override fun <T : ViewModel> create (modelClass: Class <T >) : T { return MainViewModel(countReserved) as T } }
Lifecycles 在一个非Activity的类中感知Activity生命周期。
隐式Fragment 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MyObserver { fun activityStart () { } fun activityStop () { } } class MainActivity : AppCompatActivity () { lateinit var observer: MyObserver override fun onCreate (savedInstanceState: Bundle ?) { observer = MyObserver() } override fun onStart () { super .onStart() observer.activityStart() } override fun onStop () { super .onStop() observer.activityStop() } }
Lifecycles组件 Lifecycles可以让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MyObserver : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) fun activityStart () { Log.d("MyObserver" , "activityStart" ) } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun activityStop () { Log.d("MyObserver" , "activityStop" ) } } class MainActivity : AppCompatActivity () { ... override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... lifecycle.addObserver(MyObserver()) } ... }
我们在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件。生命周期事件的类型一共有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何生命周期回调。
LiveData 简单场景处理,同时将不会成为主流了,复杂场景需要学习flow、stateflow等替代。
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。
LiveData内部不会判断即将设置的数据和原有数据是否相同,只要调用了setValue()或postValue()方法,就一定会触发数据变化事件。
LiveData之所以能够成为Activity与ViewModel之间通信的桥梁,并且还不会有内存泄漏的风险,靠的就是Lifecycles组件。LiveData在内部使用了Lifecycles组件来自我感知生命周期的变化 ,从而可以在Activity销毁的时候及时释放引用,避免产生内存泄漏的问题。
另外,由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者 。如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当Activity恢复可见状态时,只有最新的那份数据才会通知给观察者 ,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。
基本用法 注意Activity不能将实例传给ViewModel,否则会因为Activity无法释放而造成内存泄露。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class MainViewModel (countReserved: Int ) : ViewModel() { val counter = MutableLiveData<Int >() init { counter.value = countReserved } fun plusOne () { val count = counter.value ?: 0 counter.value = count + 1 } fun clear () { counter.value = 0 } } class MainActivity : AppCompatActivity () { ... override fun onCreate (savedInstanceState: Bundle ?) { ... plusOneBtn.setOnClickListener { viewModel.plusOne() } clearBtn.setOnClickListener { viewModel.clear() } viewModel.counter.observe(this , Observer { count -> infoText.text = count.toString() }) } override fun onPause () { super .onPause() sp.edit { putInt("count_reserved" , viewModel.counter.value ?: 0 ) } } }
对于Observer不能省略的原因:这是一种非常特殊的情况,因为observe()方法接收的另一个参数LifecycleOwner也是一个单抽象方法接口。当一个Java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。由于我们第一个参数传的是this,因此第二个参数就无法使用函数式API的写法了。
不过在2019年的Google I/O大会上,Android团队官宣了Kotlin First,并且承诺未来会在Jetpack中提供更多专门面向Kotlin语言的API。lifecycle-livedata-ktx就是一个专门为Kotlin语言设计的库,这个库在2.2.0版本中加入了对observe()方法的语法扩展。现在就可以使用以下写法:
1 2 3 viewModel.counter.observe(this ) { count -> infoText.text = count.toString() }
推荐写法 由于上面的做法暴露了可变的LiveData给外部,推荐永远只暴露并不可变的LiveData给外部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class MainViewModel (countReserved: Int ) : ViewModel() { val counter: LiveData<Int > get () = _counter private val _counter = MutableLiveData<Int >() init { _counter.value = countReserved } fun plusOne () { val count = _counter.value ?: 0 _counter.value = count + 1 } fun clear () { _counter.value = 0 } }
map和switchMap 看map()方法,这个方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。
1 2 3 4 5 6 7 8 class MainViewModel (countReserved: Int ) : ViewModel() { private val userLiveData = MutableLiveData<User>() val userName: LiveData<String> = Transformations.map(userLiveData) { user -> "${user.firstName} ${user.lastName} " } ... }
switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 data class User (var firstName: String, var lastName: String, var age: Int ) object Repository { fun getUser (userId: String ) : LiveData<User> { val liveData = MutableLiveData<User>() liveData.value = User(userId, userId, 0 ) return liveData } } 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 } } class MainActivity : AppCompatActivity () { ... override fun onCreate (savedInstanceState: Bundle ?) { ... getUserBtn.setOnClickListener { val userId = (0. .10000 ).random().toString() viewModel.getUser(userId) } viewModel.user.observe(this , Observer { user -> infoText.text = user.firstName }) } ...
通过getUser(userId) 改变userIdLiveData -> 在switchMap观察到userIdLiveData改变后,执行lambda表达式中的内容 -> user.observe观察到user变化,回调执行lambda表达式中内容。
userIdLiveData是对外不可见的,是不能在外部观察的;user是可见的,是可以在外部观察的,从而将这个LiveData对象转换成另外一个可观察的LiveData对象。
Room ORM也叫对象关系映射,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。
Room的整体结构。它主要由Entity、Dao和Database这3部分组成,详细说明如下。
Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
Dao。Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提 供Dao层的访问实例。
由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是Java项目的话,使用annotationProcessor即可。
Entity 1 2 3 4 5 @Entity data class User (var firstName: String, var lastName: String, var age: Int ) { @PrimaryKey(autoGenerate = true) var id: Long = 0 }
Dao 这部分是Room用法中最关键的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Dao interface UserDao { @Insert fun insertUser (user: User ) : Long @Update fun updateUser (newUser: User ) @Query("select * from User" ) fun loadAllUsers () : 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 }
但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。Room是支持在编译时动态检查SQL语句语法的,如果编写的SQL语句有语法错误,编译的时候就会直接报错。
Database 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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 } } } }
AppDatabase类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类。
databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext ,而不能使用普通的context,否则容易出现内存泄漏 的情况。
另外,由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,为了方便测试,Room还提供了一个更加简单的方法,这个方法建议只在测试环境下使用,如下所示:
1 2 3 Room.databaseBuilder(context.applicationContext, AppDatabase::class .java,"app_database" ) .allowMainThreadQueries() .build()
数据库升级 一般升级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @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)" ) } } private var instance: AppDatabase? = null 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 } } } }
注意:Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常。
数据库结构变化升级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Database(version = 3, entities = [User::class, Book::class]) abstract class AppDatabase : RoomDatabase () { ... companion object { ... val MIGRATION_2_3 = object : Migration(2 , 3 ) { override fun migrate (database: SupportSQLiteDatabase ) { database.execSQL("alter table Book add column author text not null default 'unknown'" ) } } private var instance: AppDatabase? = null 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 } } } }
比较有难度的就是编写SQL语句。
WorkManager WorkManager和Service并不相同,也没有直接的联系。
Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。
而WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等。
使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间
基本用法
定义一个后台任务,并实现具体的任务逻辑;
配置该后台任务的运行条件和约束信息,并构建后台任务请求;
将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。
定义后台任务:
1 2 3 4 5 6 class SimpleWorker (context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork () : Result { Log.d("SimpleWorker" , "do work in SimpleWorker" ) return Result.success() } }
doWork()方法不会运行在主线程当中。
配置该后台任 务的运行条件和约束信息:
这一步最为复杂。
1 2 3 val request = OneTimeWorkRequest.Builder(SimpleWorker::class .java).build() val request = PeriodicWorkRequest.Builder(SimpleWorker::class .java, 15 , TimeUnit.MINUTES).build()
调用enqueue()方法执行:
1 WorkManager.getInstance(context).enqueue(request)
处理复杂任务 WorkManager拥有以下功能:
让后台任务在指定的延迟时间后运行;
比如说给后台任务请求添加标签,可以通过标签来取消后台任务请求;
doWork()方法中返回了Result.retry(),那么是可以结合setBackoffCriteria()方法来重新执行任务的;
对运行结果进行监听;
链式任务。
setBackoffCriteria()时间设置不能小于10s,同时可以设置线性延迟或者指数的方式延迟。
这是因为绝大多数的国产手机厂商在进行Android系统定制的时候会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序既无法接收广播,也无法运行WorkManager的后台任务 。因此,这里给你的建议就是,WorkManager可以用,但是千万别依赖它去实现什么核心功能,因为它在国产手机上可能会非常不稳定 。
Kotlin课堂(了解) DSL的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。
比如Gradle是一种基于Groovy语言的构建工具,因此如下的语法结构其实就是Groovy提供的DSL功能,build.gradle文件如下:
1 2 3 4 dependencies { implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:converter-gson:2.6.1' }
我们实现Kotlin版本的DSL功能,代码如下:
1 2 3 4 5 6 7 8 9 10 11 class Dependency { val libraries = ArrayList<String>() fun implementation (lib: String ) { libraries.add(lib) } } fun dependencies (block: Dependency .() -> Unit ) : List<String> { val dependency = Dependency() dependency.block() return dependency.libraries }
好了,现在可以使用以下的语法,build.gradle文件如下:
1 2 3 4 dependencies { implementation("com.squareup.retrofit2:retrofit:2.6.1" ) implementation("com.squareup.retrofit2:converter-gson:2.6.1" ) }
这就是Kotlin版本的Gradle构建文件。