Jetpack

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_CREATEON_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声明周期对应关系如下:


    image.png

    也就是说当获取到声明周期状态为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的后台任务。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 本篇文章主要介绍以下几个知识点:ViewModelLifecyclesLiveDataRoomWorkManage...
    开心wonderful阅读 1,114评论 0 0
  • Jetpack 简介 JetPack是一个开发组件工具集,主要目的是帮助我们编写出更加简洁的代码,并简化开发过程。...
    FFFSnow阅读 402评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,454评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,534评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,705评论 1 1