1、Jetpack简介
Jetpack是一个开发组件工具集,主要由基础、架构、行为、界面四部分组成。它主要目的是帮助我们编写出更加简洁的代码,简化我们的开发流程。Jetpack中的组件不依赖任何Android系统版本,这意味着这些组件通常定义在AndroidX库(是Android Support的升级)中,并且拥有很好的向下兼容性。
目前Android官方最为推荐的项目架构是MVVM,Jetpack中许多架构组件是为MVVM架构量身打造的。
2、Jetpack主要组件
2.1、ViewModel简介
ViewModel是Jetpack中最重要的控件之一,在Android平台上之所以会出现MVC、MVP模式,就是因为在传统的模式下,Activity的任务实在太重了,它不仅要负责逻辑的处理还要负责视图的展示,甚至还要处理网络回调,而ViewModel的出现就是为了减轻Activity的工作量,它是专门存放和视图相关的数据的。也就是说只要界面上能看得到的数据,它的相关的变量都应该存在ViewModel中,而不是Activity中,这样就能减少Activity中的逻辑。
我们知道手机发生横竖旋转时,Activity会重建其中的数据也会丢失,而ViewModel的生命周期和Activity不同,它可保证在手机横竖屏旋转时不会被重建,只有当Activity退出时才会跟着Activity一同销毁。
因此,将与界面相关的数据存放在ViewModel中,即使手机发生横竖屏旋转,数据也不会丢失。
2.2、ViewModel的基本使用
下面我们就举个简单的例子来验证下,ViewModel是否能在手机发生横竖屏旋转时保存数据。
通常来讲比较好的编程规范是一个Activity或Fragment对应一个ViewModel,这里我们为MainActivity创建一个MainViewModel类继承ViewModel。
class MainViewModel : ViewModel() {
var count = 0
}
现在我们在界面上添加一个按钮,每点一次按钮count就加1,并显示在界面上。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_count"
android:layout_width="match_parent"
android:layout_height="50dp" />
<Button
android:id="@+id/btn_start_count"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="开始计数" />
</LinearLayout>
下面我们在Activity中实现计数的逻辑
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//已被弃用 可以使用ViewModelProvider来代替
// viewModel = ViewModelProviders.of(this).get<MainViewModel>(MainViewModel::class.java)
viewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
).get(MainViewModel::class.java)
btn_start_count.setOnClickListener {
viewModel.count++
refreshText()
}
refreshText()
}
private fun refreshText() {
tv_count.text = "${viewModel.count}"
}
}
这里我们并没有通过new ViewModel()的方式来创建ViewModel,否则在Activity被销毁重建时每次都会创建一个新的ViewModel对象,这样就无法实现数据的保存了。这里我们创建ViewModelProvider对象,构造函数中传入两个参数,参数1指的是ViewModelStoreOwner (AppCompatActivity和Fragment都是其子类)
直接传入Activity即可 ,参数2是 Factory factory,这里传入AndroidViewModelFactory对象即可,AndroidViewModelFactory继承Factory,重写create()方法在其中创建ViewHolder对象,并通过ViewModelProvider().get()返回。
现在运行一下程序,旋转屏幕你会发现数据并没有丢失。
2.3、向ViewModel传递参数
ViewModel的创建是通过 ViewModelProvider().get()获取的,并没有看到传递参数的地方,其实如何传递参数上面我们已经讲到了,ViewModelProvider构造函数的第二个参数是Factory的子类,在create()方法中去创建ViewModel,聪明的你肯定知道怎么传递参数了吧,下面举例说明下:
创建一个类实现Factory
class SecondViewModelFactory(val reservedCount: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SecondViewModel(reservedCount) as T
}
SecondViewModelFactory主构造函数接收的就是我们要传递的参数,然后在create方法中将参数传入到SecondViewModel的构造函数中。这样就实现了ViewModel的参数传递。
3、LifeCycles
在编写程序的时候,我们经常会遇到需要监听Activity声明周期的需求,比如请求网络,在页面关闭后请求才得到响应,这时我们就不应该继续对响应结果进行处理了。在Activity内感知Activity的生命周期是很简单的,如果在其他类中该如何感知Activity的生命周期呢?这就需要用到LifeCycles组件了,它可以让任何一个类都能轻松感知Activity的生命周期,同时也不需要在Activity写太多逻辑。
下面我们就学习下如何使用LifeCycles,首先创建一个MyObservable类并实现LifecycleObserver接口:
class MyObervable:LifecycleObserver
LifecycleObserver 是一个空方法接口,这里我们只需声明MyObervable类实现一下LifecycleObserver接口即可。
我们可以在MyObervable类中定义任何方法,如果我们想去感知Activity的生命周期,就需要借助注解来完成,示例如下:
class MyObervable : LifecycleObserver {
private val tag = javaClass.simpleName
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public fun onActivityCreate() {
Log.e(tag, "onActivityCreate")
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public fun onActivityStart() {
Log.e(tag, "onActivityStart")
}
}
上面我们使用了注解@OnLifecycleEvent
,并传入声明周期的事件,声明周期的事件一共有7种:
- ON_CREATE
- ON_START
- ON_RESUME
- ON_PAUSE
- ON_STOP
- ON_DESTROY
- ON_ANY
它们分别对应Activity的相应的生命周期,其中ON_ANY
对应任何声明周期。上面我们使用了ON_CREATE
和ON_START
这样在Activity的onCreate()和onStart()方法调用时就会触发onActivityCreate()
和onActivityStart()
的调用。
目的为止代码还是无法正常工作的,我们还需要使用LifecycleOwner
来帮助完成,我们可以使用如下代码让MyObservable接收到通知
LifecycleOwner.getLifecycle().addObserver(MyObervable())
AppCompatActiviy和androidx.fragment.app.Fragment都是LifecycleOwner的子类,所以我们可以在Activity中直接调用getLifecycle()
class LifeCyclesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_life_cycles)
lifecycle.addObserver(MyObervable())
}
}
这就是LifeCycles组件最常用的用法了,不过目前MyObservable虽然能感知Activity的生命周期发生了变化,但是没有主动获取Activity当前生命周期的方法,要解决这个问题很简单,只要将LifeCycle对象传递给MyObservable对象即可。
class MyObervable(val lifeCycle: Lifecycle) : LifecycleObserver {
private val tag = javaClass.simpleName
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public fun onActivityCreate() {
Log.e(tag, "onActivityCreate ${lifeCycle.currentState}")
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public fun onActivityStart() {
Log.e(tag, "onActivityStart")
}
}
有了LifeCycle对象,我们就能在任意地方通过调用lifeCycle.currentState
获取当前Activity的生命周期的状态了。
LifeCycle对象怎么获取呢?我们可以通过LifecycleOwner.getLifecycle()
即可得到,具体调用代码如下:
class LifeCyclesActivity : AppCompatActivity() {
lateinit var observable: MyObervable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_life_cycles)
observable = MyObervable(lifecycle)
lifecycle.addObserver(observable)
}
override fun onDestroy() {
super.onDestroy()
lifecycle.removeObserver(observable)
}
}
lifeCycle.currentState
返回的状态一共有5种:
- DESTROYED
- INITIALIZED
- CREATED
- STARTED
-
RESUMED
它们与Activity声明周期对应关系如下:
也就是说当获取到声明周期状态为CREATED时,说明onCreate()方法已经执行了,但是onStart()方法还未执行,当获取的生命周期状态为STARTED时,说明onStart()已经执行,但是onResume()还未执行,当获取到是状态为RESUMED时说明onResume()已经执行。
当Activity失去焦点时,onPause()方法会执行,此时生命周期的状态为STARTED,当Activity不可见时,onStop()方法会执行,此时生命周期的状态为CREATED,当页面被关闭时,onDestory()方法会执行,此时生命周期状态为DESTROYED。
上面我们学习的两个Jetpack的两个重要组件ViewModel和LifeCycles,它们是相互独立的,没有太多直接的关系,为了让组件更好的结合使用,下面学习Jetpack中另一个重要的组件:LiveData。
4、LiveData
LiveData是Jetpack提供的一种响应式编程组件。它可以包含任何类型的数据,并在数据发生变化时通知给观察者。LiveData虽然能单独使用,但是大多数情况下是在ViewModel中使用。
4.1、LiveData基本用法
之前我们在学习ViewModel的时候举了个例子:当每次点击按钮时,都先给ViewModel中存储的数据加1,然后立即获取最新计数去展示,这在单线程中完全没问题,但是如果在ViewModel中开启线程去执行耗时操作的话,那么点击按钮后立即去获取最新的数据,得到的肯定还是之前的数据。这是因为我们在Activity中主动去获取数据的时候数据可能还未改变呢?能不能在数据变化后通知Activity呢?
有点同学肯定会说,在创建ViewModel的时候将Activity传给ViewModel,在ViewModel中数据变化时不就可以通知Activity了,这样肯定的是不行的,ViewModel的生命周期长于Activity,很可能会造成内存泄露。
其实解决方案很简单只需要使用LiveData,LiveData中可以包含任意类型的数据,并在数据发生变化通知观察者。也就是说将计数器中的数据用LiveData进行包装,并在Activity中去观察它,就可以主动将数据变化通知给Activity了。
根据这个原理修改下ViewModel类
class LiveDataViewModel(reveredCount: Int) : ViewModel() {
var counter = MutableLiveData<Int>()
init {
counter.value = reveredCount
}
fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}
这里我们将counter修改为MutableLiveData对象,并指定其泛型为Int。MutableLiveData是一个可变的LiveData,它的用法很简单,主要有3种读写数据的方法:setValue(T value)、postValue(T value)、getValue(),setValue和postValue都是设置数据的方法,不过setValue()只能在主线程中使用,postValue()可以在子线程中使用。getValue()方法是读取数据方法,不过getValue()获取的数据可能为空,所以这里使用?:操作符,当数据为空时用0来计数。
接下来看下LiveDataActivity中调用
class LiveDataActivity : AppCompatActivity() {
private lateinit var liveDataViewModel: LiveDataViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_data)
val resverCount = getPreferences(MODE_PRIVATE).getInt("reservedCount", 0)
liveDataViewModel = ViewModelProvider(
this,
LiveDataFactory(resverCount)
).get(LiveDataViewModel::class.java)
liveDataViewModel.counter.observe(this) {
tv_count.text = it.toString()
}
btn_count.setOnClickListener {
liveDataViewModel.plusOne()
}
btn_clear.setOnClickListener {
liveDataViewModel.clear()
}
}
override fun onPause() {
super.onPause()
getPreferences(MODE_PRIVATE).edit {
putInt("reservedCount", liveDataViewModel.counter.value ?: 0)
}
}
}
其中比较重要的代码就是使用liveDataViewModel.counter.observe()
来观察数据的变化。liveDataViewModel.counter是一个LiveData对象,所以可以调用observe()方法去观察数据的变化。observe()方法接收2个参数,参数1是 LifecycleOwner,也就是Activity本身,参数2是Observe接口,数据发生变化后就会回调到其onChanged方法,我们可以在这个方法中进行UI的更新。
重新运行程序你会发现,计数器功能正常,不需要担心ViewModel中开启线程执行耗时操作,不过需要注意的是在子线程中需要使用postValue()进行数据的设置,不能使用setValue(),否则会崩溃。
以上就是LiveData的基本用法。虽然可以正常工作,但是它仍然不是最正规的LiveData的用法。主要问题是:我们将counter这个可变的LiveData暴露给外部,这样即使在ViewModel外部也可以给counter设置数据,这就违背了LiveData的封装性,同时也可能带来一定的风险。
比较推荐的做法就是:永远只暴露不可变的LiveData给外部
,这样ViewModel外部就只能观察数据变化,而不能设置数据了。下面我们修改下LiveDataViewModel类
class LiveDataViewModel(reveredCount: Int) : ViewModel() {
val counter: LiveData<Int>
get() = _counter
private val _counter = MutableLiveData<Int>()
init {
_counter.value = reveredCount
}
fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}
fun clear() {
_counter.value = 0
}
}
首先我们将原先的counter变量修改为_counter并用private修饰,这样_counter对于ViewModel外部就是不可见的了,然后我们又定义了一个不可变的LiveData将其变量命名为counter,并在它的get()属性中返回_counter对象。
这样在ViewModel外部调用counter变量时,实际获得的是_counter的对象,但是我们并不能给它设置数据,这就保证了ViewModel的数据封装性。
4.2、map和switchMap
map
map和switchMap方法都是转换LiveData的方法,map方法将实际包含数据的LiveData和需要观察的LiveData进行转换。下面举例说明下使用场景:
比如一个Student类中定义了姓名、年龄和性别,定义如下:
data class Student(var name:String,var sex:String,var age:Int)
我们在ViewModel中来创建一个可变的LiveData来包含User类型的数据:
class MainViewModel:ViewModel() {
val stuLiveData=MutableLiveData<Student>()
}
如果我们视图上只展示姓名和年龄,并不关心性别,那么把整个Student类型的LiveData暴露出去不太合适。而map方法就是解决这个问题的,它可以把Student类型的LiveData转换成其他类型的LiveData,下面看下具体用法:
class MainViewModel : ViewModel() {
private val stuLiveData: MutableLiveData<Student> = MutableLiveData<Student>()
val realLiveData: LiveData<String> = Transformations.map(stuLiveData) {
"${it.name} ${it.age}"
}
}
这里我们将可变的stuLiveData使用private修饰,将转换后的不可变的realLiveData暴露出去,这样比较符合ViewModel数据的封装性。当stuLiveData数据发生变化时,map方法可以监听到变化并执行转换的逻辑,将转换后的数据通知给realLiveData的观察者。
switchMap
前面我们学习的前提是LiveData是在ViewModel中创建的,在实际的应用中,LiveData可能在ViewModel外部获取的,下面我们就在ViewModel外部创建一个获取LiveData的方法:
object LiveDataInstance {
fun getStudentLiveData(stuId:String): LiveData<Student>{
val liveData =MutableLiveData<Student>()
liveData.value=Student(stuId,stuId,1)
return liveData
}
}
这里在LiveDataInstance 添加了一个getStudentLiveData()方法,该方法接收一个stuId参数,按正常逻辑,应该根据传入的stuId去数据库中或者请求网络查找到相应的Student对象,这里只是模拟示例,因此将每次传入的stuId当做用户名来创建一个新的Student即可。
需要注意的是getStudentLiveData()方法返回的是一个包含Student数据的LiveData数据,而且每次调用该方法都会返回一个新的LiveData实例。
然后我们在MainViewModel中定义getStudent()方法,并且让它调用LiveDataInstance.getStudentLiveData()方法返回LiveData的实例。
class MainViewModel : ViewModel() {
fun getStudent(stuId: String): LiveData<Student> {
return LiveDataInstance.getStudentLiveData(stuId)
}
}
接下来的问题就是如何在Activity中观察LiveData数据的变化,既然getStudent()方法返回的是一个LiveData的实例,我们能不能在Activity中使用如下写法:
mainViewModel.getStudent("").observe(this){
}
这样写肯定是不行的,因为每次调用LiveDataInstance.getStudentLiveData()获取的都是一个新的LiveData实例,而上述写法会一直观察老的LiveData,所以是无法观察到数据变化的。我们可以使用switchMap()方法来解决这个问题,它的用法比较固定:如果ViewModel中的某个LiveData是调用外部方法获取到的,可以使用switchMap方法将其转换成另一个可观察的LiveData。具体用法如下:
class MainViewModel : ViewModel() {
private val stuLiveData = MutableLiveData<String>()
var student: LiveData<Student> = Transformations.switchMap(stuLiveData) {
LiveDataInstance.getStudentLiveData(it)
}
fun getStudent(stuId: String) {
stuLiveData.value = stuId
}
}
这里我们定义了一个stuLiveData对象用于监听stuId的变化,然后调用switchMap将其转换成另一个可观察的LiveData对象。
switchMap接收2个参数,第一个参数传入我们新增的stuLiveData,switchMap会对其观察,第二个参数是一个转换函数,注意必须在这个转换函数中返回一个LiveData对象,因为switchMap的的工作原理就是将转换函数中返回的LiveData实例转换成另一个可观察的LiveData实例,很显然我们只需在转换函数中调用 LiveDataInstance.getStudentLiveData()方法将得到的LiveData对象返回即可。
为了更清晰的明白switchMap的用法,我们梳理下它的工作流程:
当外部调用MainViewModel的getStudent()来获取用户数据时,只需将传入的stuId设置到stuLiveData 中,一旦调用了stuLiveData.setValue()方法,那么观察stuLiveData的switchMap()就会执行,并且调用我们编写的转换函数,将 LiveDataInstance.getStudentLiveData(it)返回的LiveData转换成一个可观察的LiveData对象,对于Activity而言,只要观察这个LiveData对象即可。
下面看下在MainActivity中的调用:
class MainActivity : AppCompatActivity() {
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainViewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
).get(MainViewModel::class.java)
mainViewModel.student.observe(this){
showData(it)
}
btn_set_data.setOnClickListener {
val stuId=(1..100).random()
mainViewModel.getStudent
(stuId.toString())
}
}
private fun showData(student: Student) {
tv_first_name.text =student.name
}
}
具体用法就是这样,点击按钮会生成一个随机的id,然后调用MainViewModel的getStudent()方法来获取用户数据,此时这个方法不会有任何返回值,等数据获取成功后,可观察LiveData对象的observe()方法就会收到通知,我们在这里把数据展示在视图上。
在刚才的例子中,我们调用MainViewModel的getStudent()方法中传入了一个stuId,为了能够观察这个值的变化,我们创建了一个stuLiveData,然后在switchMap中观察stuLiveData即可。但是如果MainViewModel中获取数据的方法如果没有参数呢,该怎么观察变化呢?其实很简单,我们只需创建一个空的LiveData对象即可,具体代码如下:
class MainViewModel : ViewModel() {
private val refreshLiveData=MutableLiveData<Any?>()
var refreshResult: LiveData<Any?> =Transformations.switchMap(refreshLiveData){
LiveDataInstance.refreshStatus()
}
fun refreshStatus(){
refreshLiveData.value=refreshLiveData.value
}
}
这里在MainViewModel中定义了一个不含任何参数的refreshStatus()方法,又对应的定义了一个refreshLiveData,但是它不需要指定具体包含的数据类型,因此这里我们将LiveData的泛型指定为了Any?类型。
接下来就是点睛之笔了,在refreshStatus()方法中将refreshLiveData原有的数据取出来又重新赋值给了refreshLiveData,这样就能触发一次变换,这是因为只要我们调用了LiveData的setValue()或postValue()方法,不论设置的值是否和原先的值相同,都会触发变化。
然后我们在Activity中监听refreshResult的变化即可,只要调用了MainViewModel中的refreshStatus()方法,观察者回调中就能得到最新的数据。
学到这里,可能你会说只看到了LiveData和ViewModel结合在一起使用,好像和LifeCycles并没有什么关系。
其实并不是,LiveData之所以能成为Activity和ViewModel通信的桥梁,并且还不会有内存泄漏的风险,靠的就是LifeCycles组件。LiveData内部使用LifeCycles组件来自我感知生命周期的变化,从而可以在Activity销毁的时候释放引用,避免产生内存泄漏的问题。
另外,由于要减少性能消耗,当Activity不可见的状态时(如手机息屏或被其他Activity遮挡),如果LiveData中数据发生了变化,是不会通知观察者的,只有当Activity重新恢复可见状态时,才会将数据通知给观察者,而LiveData之所以能实现这些细节靠的还是LifeCycles组件。
还有一个小细节,如果在Activity出于不可见状态时,LiveData数据发生了多次变化,当Activity恢复可见时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于过期了,会被直接丢弃。
5、Room
由于使用原生的API对SQLite数据库进行增删改查操作,在大型项目中非常容易使项目代码变得比较混乱,为此出现了专门为Android数据库设计的ORM框架。
ORM(Object Relational Mapping)也叫对象关系映射,简单来说我们编程语言是面向对象的,而数据库是面向关系的,将面向对象的语言和面向关系的数据库中间建立的一种映射关系,这就是ORM了。
使用ORM框架,就可以使用面向对象的思想去操作数据库了,绝大数情况下不再和SQL语句打交道了,避免操作数据库的逻辑使项目代码变得混乱。Room就是官方退出的一个ORM框架,并将其加入Jetpack中。
5.1、使用Room进行增删改查
在学习使用Room之前,先来看下Room的整体架构。它主要包括:Entity、Dao和Database这3个部分:
Entity:用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,数据库中每个列都是根据实体类中的字段自动生成的。
Dao:数据访问的对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程中,逻辑层就不需要和数据库的底层打交道了,只需要和Dao层进行交互即可。
Database:定义数据库的关键信息,包括版本号、包含哪些实体类以及和提供Dao层访问的实例。
下面结合实践学习一下Room具体用法。首先在app/build.gradle文件中添加如下依赖:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}
dependencies {
....
implementation 'androidx.room:room-runtime:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
}
这里新增了一个kotlin-kapt
插件,同时在dependencies闭包中添加了两个Room依赖库。由于Room会根据项目中声明的注解生成代码,因此这里要使用kapt引入Room编译时的注解库,而启用编译时注解功能则一定要添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是java项目的话,使用annotationProcessor即可。
前面我们说了Room由3部分组成,这里先看Entity实体类的定义。
@Entity
data class Student(var name: String, var sex: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
这里在Student的类名上使用了@Entity
注解将其声明成了一个实体类,然后定义了一个Long性的id作为主键,并指定自动生成。
接着定义Dao,这是Room中最关键的地方,因为所有访问数据库的操作都在这里封装。
下面看下一个Dao的具体是如何实现的,新建一个StudentDao接口,注意必须是接口,这点和Retrofit是类似的,然后在接口中编写如下代码:
@Dao
interface StudentDao {
@Insert
fun insertStu(stu:Student):Long
@Update
fun updateStu(stu: Student)
@Query("select * from Student")
fun loadAllStudent():List<Student>
@Query("select * from Student where age > :age")
fun loadStudentOldThan(age:Int):List<Student>
@Delete
fun deleteStudent(stu: Student)
@Query("delete from Student where lastName = :lastName")
fun deleteStuByLastName(lastName:String):Int
}
StudentDao类上面使用了一个@Dao注解,这样Room才能将它识别成一个Dao,StudentDao的内部根据业务需求对数据库的各种操作进行了封装。数据库的操作通常有增删改查这4种,因此Room也提供了@Insert、@Delete、@Update、@Query这4种相应的注解。
从上面的类中可以看出在使用注解@Insert、@Delete、@Update时并没有使用SQL语句,而只是使用了注解标识。如果要想从数据库中查找数据或者使用非实体类进行增删改的话,就必须使用SQL语句。在使用非实体参数进行增删改时,还可以使用: 非实体类参数
的形式引入到SQL语句中。
最后来看下Database的定义,在其中需要定义数据库的版本号、包含哪些数据库以及提供给Dao层访问的实例
@Database(version = 1, entities = [Student::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun getStuDao(): StudentDao
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类的头部使用了@Database注解,并在注解中声明了数据库的版本号,包含哪些实体类,多个实体类使用逗号进行分隔。
另外AppDatabase必须继承RoomDatabase类,并且一定要使用abstract将类定义成抽象类,然后提供相应的抽象方法,用于获取之前编写的Dao的实例,比如这里提供的getStuDao()方法。不过我们只需要进行方法的声明就可以了,具体的方法实现由Room在底层完成的。
紧接着我们在companion object
结构体中编写了一个单例模式,因为原则上全局应该只存在一个AppDatabase的实例。这里使用了instance变量来缓存AppDatabased,然后在getDataBase()方法中判断,如果instance不为null则直接返回,如果为null则使用Room.databaseBuilder()去创建,Room.databaseBuilder()接收3个参数,注意第一个参数必须传ApplicationContext,而不能使用普通的Context,否则会造成内存泄漏。第二个参数是AppDatabase的class类型,第三个参数传入的是数据库名称。最后通过build()创建实例,并将其赋值给instance。
Room的三个部分已经定义完成了,下面我们测试一下:
class MainActivity : AppCompatActivity() {
private lateinit var stuDao: StudentDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
stuDao = AppDatabase.getDataBase(this).getStuDao()
val job = Job()
val scope = CoroutineScope(job)
val stu1 = Student("王一", "一", 1)
val stu2 = Student("王二", "二", 2)
btn_insert.setOnClickListener {
scope.launch {
stu1.id = stuDao.insertStu(stu1)
stu2.id = stuDao.insertStu(stu2)
}
}
btn_delete.setOnClickListener {
scope.launch {
stuDao.deleteStudent(stu1)
stuDao.deleteStuByLastName("一")
}
}
btn_update.setOnClickListener {
scope.launch {
stu1.age = 3
stuDao.updateStu(stu1)
}
}
btn_query.setOnClickListener {
scope.launch {
val stuList1 = stuDao.loadAllStudent()
for (stu in stuList1) {
Log.e("MainActivity", stu.toString())
}
}
}
}
}
注意:在添加数据时,将insertStu()方法的返回值赋给原来的对象,否则更新和删除是失败的,因为@Update和@Delete注解去更新和删除数据时都是基于这个id值来操作的。
另外需要的注意的是对于数据库的操作属于耗时操作,Room不允许在主线程进行数据库的操作。
5.2、Room数据库的升级
Room数据库升级设计的比较麻烦,基本上没有比原生的SQLiteDatabase简单到哪去,每次升级都需要手动编写升级逻辑。不过Room倒是提供了一个简单粗暴的方法,如下所示:
Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"app_database")
.fallbackToDestructiveMigration()
.build()
这样只要数据进行了升级就会自动销毁并重新创建,其中的数据也会丢失。
下面我们看下Room升级正确的写法。
随着业务的升级,打算在数据库添加一张Book表,那么就需要创建一个Book的实体类。
@Entity
data class Book(var name: String, var pages: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
接着创建BookDao接口,并定义一些方法
@Dao
interface BookDao {
@Insert
fun insertBook(book:Book):Long
@Query("select * from Book")
fun loadAllBooks()
}
最后修改下之前的AppDatabase类,增加getBookDao()抽象方法,并把数据库的版本号改为2,并在其中加入数据库升级的逻辑
@Database(version = 1, entities = [Student::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun getStuDao(): StudentDao
abstract fun getBookDao(): BookDao
companion object {
//升级的操作
val MIGATION_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
@Synchronized
fun getDataBase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).addMigrations(MIGATION_1_2)
.build().apply {
instance = this
}
}
}
}
其中升级最关键的地方是:在companion object结构体中,我们实现了一个Migation的匿名类,并传入1和2两个参数,表示当前数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。由于我们需要添加一张Book表,所以我们需要在migate()方法中编写相应的建表语句。最后在构建AppDatabase实例的时候,加入一个addMigration()方法,并将MIGRATION_1_2传入即可。
现在我们又想向Book表中添加一个作者字段,代码如下:
首先在实体类中增加作者字段
@Entity
data class Book(var name: String, var pages: Int,var author:String) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
然后修改AppDatabase:
@Database(version = 3, entities = [Student::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun getStuDao(): StudentDao
abstract fun getBookDao(): BookDao
companion object {
//升级的操作
private val MIGATION_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 val MIGATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("alter table Book add column author text not null default 'unkonwn'")
}
}
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(MIGATION_1_2)
.addMigrations(MIGATION_2_3)
.build().apply {
instance = this
}
}
}
}
和之前步骤类似,此处不再赘述。
6、WorkManager
Android后台机制是一个很复杂的话题,随着系统版本的升级Android官方对后台的限制越来越严格。从Android4.4开始AlarmManager的触发时间由原先的精准变成不精准,5.0又增加了JobScheduler处理后台任务,6.0引入了Doze和AppStandBy模式用于降低手机被后台唤醒的频率,从8.0系统开始禁止后台Service的运行,只允许使用前台Service。
那么该如何编写代码才能兼容不同的系统版本呢?为此,Google推出了WorkManager组件。WorkManager很适用于处理一些要求定时执行的任务,它可以根据不同的操作系统自动选择底层是使用AlarmManager实现还是JobScheduler实现。 另外它还支持周期性任务、链式任务处理功能。
WorkManager和Service并不相同,也没有直接的联系,Service在没有被销毁的情况下是一直保持在后台运行的。而WorkManager只是一个处理定时任务的工具,它可以保证在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行,因此WorkManager很适合执行一些定期和服务器进行交互的任务,比如周期性的同步数据。
另外使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能将触发时间临近的几个任务放在一起执行,这样就可以大幅减少CPU被唤醒的次数,从而有效延长电池的使用时间。
6.1、WorkManager的基本用法
使用WorkManager前需要在app/build.gradle文件中添加以下依赖:
dependencies {
implementation 'androidx.work:work-runtime:2.2.0'
}
添加完依赖就可以使用了,WorkManager的使用步骤主要有如下3步:
- 1、定义一个后台任务,并实现具体的任务逻辑。
- 2、配置该后台任务的运行条件和约束信息,并构建后台任务的请求
- 3、将后台任务传入WorkManager的enqueue()方法中,系统在合适的时间运行
首先定义一个后台任务,这里创建一个SimpleWorker类,代码如下:
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private val tag = javaClass.simpleName
override fun doWork(): Result {
Log.e(tag, "do work in simpleworker")
return Result.success()
}
}
这个后台任务类必须继承Worker,并重写doWork()方法,在方法中编写具体的任务逻辑。
doWork不会运行在主线程中,另外doWork方法要求返回一个Result对象,用于表示任务运行的结果,成功则返回Result.success(),失败则返回Result.failure()。除此之外,Result.retry()其实也表示失败,只是可以结合WorkResult.Builder的setBackoffCriteria()方法来重新执行任务。
接着进入第二步:配置后台任务的运行条件和约束信息。这一步其实也是最复杂的一步,因为可配置的内容太多了,不过目前先学习下基本用法。
val request=OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
只需要把刚才创建的后台任务对应的Class对象传入OneTimeWorkRequest.Builder的构造函数中,然后build()即可完成构建。
OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单次运行的后台任务请求,另外它的另一个子类PeriodicWorkRequest.Builder是用于构建周期性运行的后台任务请求,但是为了降低设备的性能损耗,PeriodicWorkRequest.Builder构造函数中传入的周期间隔不能低于15分钟。示例代码如下:
val request=PeriodicWorkRequest.Builder(SimpleWorker::class.java,15,TimeUnit.MINUTES).build()
最后一步将构建的任务请求加入到WorkManager的enqueue()方法中,系统就会在合适的时间运行了,
WorkManager.getInstance(this).enqueue(request)
整体的用法就是这样,下面测试一下:使用上面我们创建好的后台任务构建Request并把它加到WorkManager中
btn_work_manager.setOnClickListener {
//配置后台任务的运行条件和约束信息,并构建后台任务请求
val request=OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
//将后台任务请求加入到WorkManager的enqueue()方法中,等待执行
WorkManager.getInstance(this).enqueue(request)
}
在点击按钮时可以看到Logcat打印了如下日志:
E/SimpleWorker: do work in simpleworker
后台任务的具体运行时间根据约束条件和系统自身的优化决定的,由于这里没有设定约束条件,所以后台任务在点击后就立刻运行了。
6.2、WorkManager处理复杂的任务
在上面的示例中我们并不能控制后台任务运行的时间,因此并有什么实际用处。下面看下如何给后台任务添加约束条件。
控制后台任务延时执行
val request=OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(20,TimeUnit.SECONDS).build()
给后台任务增加标签
val request=OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.addTag("onTime")
.build()
在添加了标签后,我们就能通过标签去取消后台任务的执行了
WorkManager.getInstance(this).cancelAllWorkByTag("oneTime")
除此之前,我们还可以通过id进行后台任务的取消
WorkManager.getInstance(this).cancelWorkById(request.id)
使用标签的好处就在于我们可以取消所有具有相同标签的后台任务,而通过id只能取消单个后台任务。
除此之外我们还可以取消全部后台任务
WorkManager.getInstance(this).cancelAllWork()
后台任务的重新执行
上面我们在创建后台任务时doWork()
方法返回Result.retry()
时,可以通过setBackoffCriteria()
重新执行任务,代码如下:
val request=OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setBackoffCriteria(BackoffPolicy.LINEAR,10,TimeUnit.SECONDS)
.build()
setBackoffCriteria()
方法接收3个参数,第二个和第三个参数表示多久后开始执行,时间最短不能少于10秒钟。第一个参数表示如果任务再次失败,下次重试的时间以何种形式延迟,可选值有2个LINEAR和EXPONENTIAL
,LINEAR表示下次重试的时间以线性方式延迟,EXPONENTIAL表示下次重试的时间以指数性方式延迟。
后台任务中doWork()中返回值的意义
在了解了Result.retry()
返回值的意义后,我们看下当返回Result.success()和Result.failure()
有啥用。其实它们用于通知后台任务执行结果的,可以通过如下代码对执行结果进行监听:
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id)
.observe(this){
when(it.state){
WorkInfo.State.SUCCEEDED->"成功".showToast(this)
WorkInfo.State.FAILED->"失败".showToast(this)
WorkInfo.State.ENQUEUED->"任务加入队列中".showToast(this)
WorkInfo.State.BLOCKED->"任务阻塞中".showToast(this)
WorkInfo.State.RUNNING->"任务执行中".showToast(this)
}
}
这里调用了getWorkInfoByIdLiveData()
传入后台任务id,会返回一个LiveData对象。然后我们可以调用LiveData的observe方法观察数据的变化了,以此监听后台任务运行的结果。
另外我们还可以调用 getWorkInfosByTagLiveData()
监听相同标签下所有后台任务请求的运行结果。
链式任务
加入后台定义了3个任务:同步数据、压缩数据和上传数据。现在我们想实现先同步、再压缩,最后上传的功能 ,就可以借助链式任务来实现,代码如下:
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()
beginWith()
方法用于开启一个链式任务,后面可以使用then()
链接后续任务。WorkManager要求必须在前一个任务成功之后,下一个任务才会执行,如果前一个任务运行失败或被取消了,接下来的任务就得不到执行。
另外需要注意的是:前面所介绍的WorkManager的所有功能在国产手机上可能无法达到预期的效果,因为绝大多数国产手机厂商在进行Android系统定制的时候回增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序是无法运行WorkManager的后台任务。