第一行代碼讀書筆記 15 -- 探究 Jetpack

本篇文章主要介紹以下幾個(gè)知識(shí)點(diǎn):

夏日 (圖片來源于網(wǎng)絡(luò))

Jetpack 是一個(gè)開發(fā)組件的工具集,其作用主要是編寫出更簡潔的代碼,簡化開發(fā)過程。這些組件通常是定義在 AndroidX 庫當(dāng)中的。

官方 Jetpack 目前的全家福如下:

Jetpack 全家福

目前 Android 官方最為推薦的項(xiàng)目架構(gòu)是 MVVM,而 Jetpack 中的許多架構(gòu)組件是專門為 MVVM 架構(gòu)量身打造的。

1. ViewModel

在傳統(tǒng)的開發(fā)模式下,Activity 扮演著多重角色,既要負(fù)責(zé)邏輯處理,又要控制 UI 展示,甚至還要處理網(wǎng)絡(luò)回調(diào)等,在大型項(xiàng)目中顯得過于臃腫且難以維護(hù)。

ViewModel 是專門用于存放與界面相關(guān)的數(shù)據(jù)的,因而可以幫助 Activity 分擔(dān)一部分工作。

另外,ViewModel 的生命周期和 Activity 的不同,它可以保證在手機(jī)屏幕旋轉(zhuǎn)時(shí)不會(huì)被重建,只有當(dāng) Activity 退出時(shí)才會(huì)跟著 Activity 一起銷毀:

ViewModel 生命周期

1.1 ViewModel 的基本用法

首先在 build.gradle 中添加依賴:

implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

一般來說,會(huì)給每一個(gè) Activity 和 Fragment 都創(chuàng)建一個(gè)對應(yīng)的 ViewModel,如給 MainActivity 創(chuàng)建一個(gè)對應(yīng)的 MainViewModel 如下:

class MainViewModel : ViewModel() { }

舉個(gè)栗子,要實(shí)現(xiàn)一個(gè)計(jì)數(shù)器的功能,就可以在 ViewModel 中加入一個(gè)變量:

class MainViewModel : ViewModel() {
    var counter = 0   // 用于計(jì)數(shù)
}

界面上一個(gè)按鈕和一個(gè)顯示計(jì)數(shù)的 TextView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/counterText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="32sp" />

    <Button
        android:id="@+id/plusButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="plus one" />

</LinearLayout>

創(chuàng)建 ViewModel 的實(shí)例需要通過 ViewModelProvider 來獲取,其語法規(guī)則如下:

ViewModelProvider(<你的 Activity 或 Fragment 實(shí)例>).get(<你的 ViewModel>::class.java)

接下來在 MainActivity 中創(chuàng)建 ViewModel 的實(shí)例和計(jì)數(shù)器邏輯:

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 獲取 ViewModel 實(shí)例
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 點(diǎn)擊按鈕計(jì)數(shù)器加 1 并刷新計(jì)數(shù)
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        // 顯示當(dāng)前計(jì)數(shù)
        counterText.text = viewModel.counter.toString()
    }
}

注: 通過 ViewModelProvider 來獲取實(shí)例是因?yàn)?ViewModel 有其獨(dú)有的生命周期,其生命周期長于 Activity,若在 onCreate() 直接創(chuàng)建 ViewModel 的實(shí)例,那么每次 onCreate() 時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例,那手機(jī)屏幕旋轉(zhuǎn)時(shí)就無法保留其中的數(shù)據(jù)了。

以上便是 ViewModel 的基本用法了。

1.2 向 ViewModel 傳遞參數(shù)

現(xiàn)有個(gè)需求,在程序退出時(shí)保存當(dāng)前的計(jì)數(shù),重新打開程序時(shí)讀取保存的計(jì)數(shù),并傳遞給 MainViewModel,那么可以修改 MainViewModel 如下:

// 在構(gòu)造函數(shù)中添加 countReserved 參數(shù)用于記錄之前保存的計(jì)數(shù)
class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = countReserved   // 用于計(jì)數(shù)
}

ViewModel 的構(gòu)造函數(shù)中傳遞參數(shù)需要借助 ViewModelProvider.Factory,其實(shí)現(xiàn)如下:

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(countReserved) as T
    }
}

注:ViewModelProvider.Factory 接口要求必須實(shí)現(xiàn) create() 方法,這邊可以創(chuàng)建 MainViewModel 的實(shí)例是因?yàn)?create() 方法的執(zhí)行時(shí)機(jī)和 Activity 的生命周期無關(guān),不會(huì)產(chǎn)生之前提到的問題。

接著修改 MainActivity 的代碼如下:

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 獲取 SharedPreferences 實(shí)例,讀取之前保存的計(jì)數(shù)
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt(SP_COUNT_RESERVED, 0)

        // 傳入保存的計(jì)數(shù)并獲取 ViewModel 實(shí)例
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 點(diǎn)擊按鈕計(jì)數(shù)器加 1 并刷新計(jì)數(shù)
            viewModel.counter++
            refreshCounter()
        }
        clearButton.setOnClickListener {
            // 點(diǎn)擊按鈕計(jì)數(shù)器清零并刷新計(jì)數(shù)
            viewModel.counter = 0
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        // 顯示當(dāng)前計(jì)數(shù)
        counterText.text = viewModel.counter.toString()
    }

    override fun onPause() {
        super.onPause()
        // 退出保存計(jì)數(shù)
        sp.edit {
            putInt(SP_COUNT_RESERVED, viewModel.counter)
        }
    }

    companion object {
        const val SP_COUNT_RESERVED = "count_reserved"
    }
}

這樣,退出程序并重新打開計(jì)數(shù)器的值就不會(huì)丟失了,只能點(diǎn)擊 Clear 按鈕才會(huì)清零。

2. Lifecycles

若有個(gè)需求,在一個(gè)非 Activity 的類中去感知 Activity 的生命周期,那么可以通過在 Activity 中嵌入一個(gè)隱藏的 Fragment 來進(jìn)行感知,或通過手寫監(jiān)聽器的方式來進(jìn)行感知等。

比如通過手寫監(jiān)聽器的方式來對 Activity 的生命周期進(jìn)行感知:

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()
    }
}

上面這種方式雖然可以工作,但不夠優(yōu)雅,需要在 Activity 中編寫大量的邏輯。

Lifecycles 組件就是為了解決這個(gè)問題出現(xiàn)的,它可以讓任何一個(gè)類輕松感知到 Activity 的生命周期,同時(shí)無需在 Activity 中編寫大量的邏輯處理。

下面舉個(gè)栗子來說明 Lifecycles 組件的用法,新建一個(gè) MyObserver 類實(shí)現(xiàn) LifecycleObserver 接口:

class MyObserver : LifecycleObserver {
    // LifecycleObserver 是個(gè)空接口,只需接口實(shí)現(xiàn)聲明,無需重寫任何方法
}

接下來就可以在 MyObserver 中定義任何方法,若需要感知 Activity 的生命周期,還需借助額外的注解,如定義 activityStart()activityStop() 方法如下:

class MyObserver : LifecycleObserver {

    // 生命周期的類型有 7 種:
    // ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY 對應(yīng) Activity 中相應(yīng)的生命周期回調(diào)
    // ON_ANY 表示可匹配 Activity 的任何生命周期回調(diào)
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart() {
        Log.d("MyObserver", "activityStart")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun activityStop() {
        Log.d("MyObserver", "activityStop")
    }
}

這樣,上面的 activityStart()activityStop() 方法就會(huì)分別在 Activity 的 onStart()onStop() 觸發(fā)時(shí)執(zhí)行。

此時(shí),當(dāng) Activity 的生命周期發(fā)生變化時(shí)并不會(huì)去通知 MyObserver,還需借助 LifecycleOwner 讓它得到通知,其語法結(jié)構(gòu)如下:

// 調(diào)用 LifecycleOwner 的 getLifecycle() 方法得到 Lifecycle 對象
// 然后調(diào)用它的 addObserver() 方法來觀察 LifecycleOwner 的生命周期
// 再把 MyObserver 的實(shí)例傳進(jìn)去
lifecycleOwner.lifecycle.addObserver(MyObserver())

對于 LifecycleOwner,只要 Activity 是繼承自 AppCompatActivity 或 Fragment 是繼承自 androidx.fragment.app.Fragment 的,那么它本身就是一個(gè) LifecycleOwner 實(shí)例,即在 Activity 中直接調(diào)用即可:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 傳入 MyObserver 的實(shí)例就可以自動(dòng)感知 Activity 的生命周期了
        lifecycle.addObserver(MyObserver())
    }
}

上面 MyObserver 雖然可以感知 Activity 是生命周期,卻無法主動(dòng)獲取當(dāng)前的生命周期狀態(tài),若需要主動(dòng)獲取則可在 MyObserver 的構(gòu)造方法中傳人 Lifecycle 對象:

class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
   ...
}

有了 Lifecycle 對象后就可在任何地方調(diào)用 lifecycle.currentState 來主動(dòng)獲取當(dāng)前的生命周期狀態(tài),lifecycle.currentState 返回的是一個(gè)枚舉類型:

// 共 5 種狀態(tài):
INITIALIZED、DESTROYED、CREATED、STRARED、RESUMED

它們與 Activity 的生命周期回調(diào)所對應(yīng)關(guān)系如下:

Activity 生命周期狀態(tài)與事件的對應(yīng)關(guān)系

3. LiveData

LiveData 是 Jetpack 提供的一種響應(yīng)式編程組件,它可包含任何類型的數(shù)據(jù),并在數(shù)據(jù)發(fā)生變化時(shí)通知給觀察者。

3.1 LiveData 的基本用法

前面計(jì)數(shù)器的栗子中,是在 Activity 中手動(dòng)獲取 ViewModel 中的數(shù)據(jù),但 ViewModel 卻無法將數(shù)據(jù)的變化主動(dòng)通知給 Activity。

注:由于 ViewModel 的生命周期長于 Activity,若把 Activity 的實(shí)例傳給 ViewModel 就可能會(huì)因?yàn)?Activity 無法釋放而造成內(nèi)存泄漏。

而用 LiveData 來包裝計(jì)數(shù)器的計(jì)數(shù),然后在 Activity 中去觀察它,就可以將數(shù)據(jù)變化通知給 Activity 了。

修改 MainViewModel 的代碼如下:

class MainViewModel(countReserved: Int) : ViewModel() {

    // MutableLiveData 是一種可變的 LiveData,它主要有 3 種讀寫數(shù)據(jù)的方法:
    // getValue() 用于獲取 LiveData 中包含的數(shù)據(jù)
    // setValue() 用于給 LiveData 設(shè)置數(shù)據(jù),但只能在主線程中調(diào)用
    // postValue() 用于在非主線程中給 LiveData 設(shè)置數(shù)據(jù)
    var counter = MutableLiveData<Int>()

    init {
        counter.value = countReserved
    }

    // 計(jì)數(shù)器加 1
    fun plusOne() {
        val count = counter.value ?: 0
        counter.value = count + 1
    }

    // 計(jì)數(shù)器清零
    fun clear() {
        counter.value = 0
    }
}

接著修改 MainActivity 的代碼如下:

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycle.addObserver(MyObserver(lifecycle))

        // 獲取 SharedPreferences 實(shí)例,讀取之前保存的計(jì)數(shù)
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt(SP_COUNT_RESERVED, 0)

        // 獲取 ViewModel 實(shí)例
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 點(diǎn)擊按鈕計(jì)數(shù)器加 1
            viewModel.plusOne()
        }
        clearButton.setOnClickListener {
            // 點(diǎn)擊按鈕計(jì)數(shù)器清零
            viewModel.clear()
        }

        // 調(diào)用 observe 觀察數(shù)據(jù)變化
        // observe() 方法接收兩個(gè)參數(shù):LifecycleOwner 對象 和 Observer 接口
        viewModel.counter.observe(this, Observer { count ->
            counterText.text = count.toString()
        })
    }

    override fun onPause() {
        super.onPause()
        // 保存計(jì)數(shù)
        sp.edit {
            putInt(SP_COUNT_RESERVED, viewModel.counter.value ?: 0)
        }
    }

    companion object {
        const val SP_COUNT_RESERVED = "count_reserved"
    }
}

注:上面 observe() 方法中沒有用到函數(shù)式 API 的寫法,當(dāng)一個(gè) Java 方法同時(shí)接收兩個(gè)單抽象方法接口參數(shù)時(shí),要么同時(shí)使用函數(shù)式 API 寫法,要么都不用。由于第一個(gè)參數(shù)傳 this,因此第二個(gè)參數(shù)就無法使用函數(shù)式 API 了。

當(dāng)然若添加如下依賴庫:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

上面的 observe() 方法就可改為:

viewModel.counter.observe(this) { count ->
    counterText.text = count.toString()
}

以上就是 LiveData 的基本用法,但不是很規(guī)范,因?yàn)榘?counter 這個(gè)可變的 LiveData 暴露給了外部,這樣在 ViewModel 外面也可給 counter 設(shè)置數(shù)據(jù),從而破壞了 ViewModel 的封裝性,有一定的風(fēng)險(xiǎn)。

比較推薦的做法是,永遠(yuǎn)只暴露不可變的 LiveData 給外部,修改 MainViewModel 如下:

class MainViewModel(countReserved: Int) : ViewModel() {

    val counter: LiveData<Int> get() = _counter

    // _counter 對外部不可見
    private val _counter = MutableLiveData<Int>()

    init {
        _counter.value = countReserved
    }

    // 計(jì)數(shù)器加 1
    fun plusOne() {
        val count = _counter.value ?: 0
        _counter.value = count + 1
    }

    // 計(jì)數(shù)器清零
    fun clear() {
        _counter.value = 0
    }
}

這樣,當(dāng)外部調(diào)用 counter 變量時(shí),實(shí)際獲得的是 _counter 的實(shí)例,但無法給 counter 設(shè)置數(shù)據(jù),從而保證了 ViewModel 的數(shù)據(jù)封裝性。

3.2 map 和 switchMap

LiveData 為了應(yīng)付不同的場景需求,提供了兩種轉(zhuǎn)換方法:map()switchMap() 方法。

  • map() 方法: 將實(shí)際包含數(shù)據(jù)的 LiveData 和僅用于觀察數(shù)據(jù)的 LiveData 進(jìn)行轉(zhuǎn)換。

舉個(gè)栗子,定義一個(gè) User 類如下:

data class User(var firstName: String, var lastName: String, var age: Int)

將 User 對象轉(zhuǎn)換成一個(gè)只包含用戶姓名的字符串暴露給外部,就可以使用 map() 方法:

class MainViewModel : ViewModel() {

    private val _userLiveData = MutableLiveData<User>()
    // 當(dāng) _userLiveData 數(shù)據(jù)變化時(shí),map() 方法會(huì)監(jiān)聽到并執(zhí)行轉(zhuǎn)換函數(shù)中的邏輯,
    // 然后把轉(zhuǎn)換后的數(shù)據(jù)通知給 userName 的觀察者
    val userName: LiveData<String> = Transformations.map(_userLiveData) {
        "${it.firstName} ${it.lastName}"
    }
}
  • switchMap() 方法:將轉(zhuǎn)換函數(shù)中返回的 LiveData 對象轉(zhuǎn)化成另外一個(gè)可觀察的 LiveData 對象。

舉個(gè)栗子,模擬 ViewModel 中的某個(gè) LiveData 對象是調(diào)用另外的方法獲取的,首先新建個(gè)單例類如下:

object Repository {
    // 模擬根據(jù) userId 獲取 User 對象
    // 每次調(diào)用 getUser() 方法都會(huì)返回一個(gè)新的 LiveData 實(shí)例
    fun getUser(userId: String): LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value = User(userId, userId, 18)
        return liveData
    }
}

借助 switchMap()方法,修改 MainViewModel 如下:

class MainViewModel : ViewModel() {

    // 用于觀察 userId 的數(shù)據(jù)變化
    private val userIdLiveData = MutableLiveData<String>()

    // 傳入 userId,switchMap() 方法會(huì)對它進(jìn)行觀察
    val user: LiveData<User> = Transformations.switchMap(userIdLiveData){
        Repository.getUser(it)
    }

    // 把傳入的 userId 設(shè)置給 userIdLiveData, userIdLiveData 變化后 switchMap() 方法就會(huì)執(zhí)行
    // 將 Repository.getUser() 返回的 LiveData 對象轉(zhuǎn)換成一個(gè)可觀察的 LiveData 對象
    fun getUser(userId: String){
        userIdLiveData.value = userId
    }
}

對于 Activity 來說,只有觀察 user 這個(gè) LiveData 對象即可,調(diào)用如下:

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)

        getUserButton.setOnClickListener {
            // 點(diǎn)擊按鈕傳入隨機(jī)數(shù)作 userId 獲取用戶數(shù)據(jù)
            // 數(shù)據(jù)獲取完成后,可觀察的 LiveData 對象的 observe() 方法將會(huì)得到通知,顯示名字
            val userId = (0..10000).random().toString()
            viewModel.getUser(userId)
        }

        viewModel.user.observe(this){
            userText.text = it.firstName
        }     
    }
}

小結(jié):LiveData 之所以能成為 Activity 與 ViewModel 之間通信的橋梁,并且不會(huì)有內(nèi)存泄漏的風(fēng)險(xiǎn),靠的是 Lifecycles 組件。LiveData 內(nèi)部使用了 Lifecycles 組件來自我感知生命周期的變化,從而在 Activity 銷毀時(shí)及時(shí)釋放引用。

另外,當(dāng) Activity 不可見狀態(tài)時(shí),若 LiveData 的數(shù)據(jù)發(fā)生了變化,是不會(huì)通知給觀察者的,只有當(dāng) Activity 恢復(fù)可見狀態(tài)時(shí),才會(huì)通知(只有最新的那份數(shù)據(jù)才會(huì)通知給觀察者),減少性能消耗。

4. Room

ORM(Object Relational Mapping) 叫對象關(guān)系映射。將面向?qū)ο蟮恼Z言和面向關(guān)系的數(shù)據(jù)庫之間建立一種映射關(guān)系,就是 ORM 了,Jetpack 中的 Room 就是一個(gè) ORM 框架。

4.1 使用 Room 進(jìn)行增刪改查

Room 主要由3部分組成:

  • Entity :定義封裝實(shí)際數(shù)據(jù)的實(shí)體類,每個(gè)實(shí)體類都會(huì)在數(shù)據(jù)庫中有一張對于的表,并且表中的列是根據(jù)實(shí)體類中的字段自動(dòng)生成的。

  • Dao:數(shù)據(jù)訪問對象,封裝對數(shù)據(jù)庫的各項(xiàng)操作,在編程時(shí)邏輯層直接和 Dao 層交互即可。

  • Database:定義數(shù)據(jù)庫中的關(guān)鍵信息,包括數(shù)據(jù)庫版本號(hào)、包含的實(shí)體類以及提供 Dao 層的訪問實(shí)例。

要使用 Room,需要在 build.gradle 中添加如下依賴:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

dependencies {
    // Room
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
}

下面舉個(gè)栗子來實(shí)現(xiàn) Room 的3個(gè)組成部分,首先是用 @Entity 注解來定義 Entity 實(shí)體類如下:

@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
    // 把 id 字段設(shè)為主鍵,并自動(dòng)生成
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

接著使用 @Dao 注解來定義 Dao,新建 UserDao 接口如下:

@Dao
interface UserDao {

    // 插入對象
    @Insert
    fun insertUser(user: User): Long

    // 更新對象
    @Update
    fun updateUser(newUser: User)

    // 查詢?nèi)繉ο?    @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)

    // 通過 lastName 來刪除對象
    @Query("delete from User where lastName = :lastName")
    fun deleteUserByLastName(lastName: String): Int
}

最后,使用 @Database 注解來定義 Database,只需定義好數(shù)據(jù)庫版本號(hào)、包含哪些實(shí)體類、提供 Dao 層的訪問實(shí)例,如下:

// 注解中聲明版本號(hào)及包含的實(shí)體類,多個(gè)實(shí)體類用逗號(hào)隔開即可
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        private var instance: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            // 不為空直接返回
            instance?.let { return it }
            // 否則構(gòu)建一個(gè)實(shí)例
            return Room.databaseBuilder(
                context.applicationContext,// 第一個(gè)參數(shù)一定要使用 applicationContext,否至容易出現(xiàn)內(nèi)存泄漏
                AppDatabase::class.java,   // 第二個(gè)參數(shù)是 AppDatabase 的 Class 類型
                "app_database"             // 第三個(gè)參數(shù)是數(shù)據(jù)庫名
            ).build().apply {
                instance = this
            }
        }
    }
}

在 Activity 中的使用如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 獲取 UserDao 的實(shí)例
        val userDao = AppDatabase.getDatabase(this).userDao()
        val user1 = User("李", "小蘭", 18)
        val user2 = User("王", "小紅", 10)

        // 新增
        addUserButton.setOnClickListener {
            thread {
                user1.id = userDao.insertUser(user1)
                user2.id = userDao.insertUser(user2)
            }
        }
        // 更新
        updateUserButton.setOnClickListener {
            thread {
                user1.age = 20
                userDao.updateUser(user1)
            }
        }
        // 刪除
        deleteUserButton.setOnClickListener {
            thread {
                userDao.deleteUserByLastName("小紅")
            }
        }
        // 查詢
        queryUserButton.setOnClickListener {
            thread {
                for (user in userDao.loadAllUsers()) {
                    Log.d("MainActivity", user.toString())
                }
            }
        }
    }
}

4.2 Room 的數(shù)據(jù)庫升級(jí)

在開發(fā)測試階段,在構(gòu)建 AppDatabase 實(shí)例時(shí),添加 fallbackToDestructiveMigration() 方法如下:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
    .allbackToDestructiveMigration()
    .build()

這樣只要數(shù)據(jù)庫進(jìn)行了升級(jí), Room 就會(huì)將當(dāng)前數(shù)據(jù)庫銷毀(數(shù)據(jù)全丟失了),再重建,實(shí)現(xiàn)自動(dòng)升級(jí)數(shù)據(jù)庫,但這只適合用在開發(fā)測試階段,下面介紹在 Room 中升級(jí)數(shù)據(jù)庫的正規(guī)寫法。

如在數(shù)據(jù)庫中添加一張 Book 表如下:

@Entity
data class Book(var name: String, var page: 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>
}

接下來要升級(jí)數(shù)據(jù)庫,需要修改 AppDatabase 如下:

// 將版本號(hào)升級(jí)成了2,并將 Book 類添加到實(shí)體類聲明中
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    abstract fun bookDao(): BookDao

    companion object {

        // 當(dāng)數(shù)據(jù)庫版本從 1 升級(jí)到 2 時(shí)就執(zhí)行這個(gè)匿名類 Migration 中的邏輯
        private 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)   // 傳入 MIGRATION_1_2
                .build().apply {
                    instance = this
                }
        }
    }
}

這樣 Room 就會(huì)自動(dòng)根據(jù)當(dāng)前數(shù)據(jù)庫的版本號(hào)執(zhí)行升級(jí)邏輯,讓數(shù)據(jù)庫始終是最新版本。

當(dāng)向現(xiàn)有表中添加新的列而不是新增一張表時(shí),使用 alert 語句修改表結(jié)構(gòu)即可,如在 Book 中添加一個(gè) author 字段:

@Entity
data class Book(var name: String, var page: Int, var author: String) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

那么對應(yīng)的數(shù)據(jù)庫表升級(jí),修改 AppDatabase 如下:

// 將版本號(hào)升級(jí)成了3
@Database(version = 3, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    abstract fun bookDao(): BookDao

    companion object {

        // 當(dāng)數(shù)據(jù)庫版本從 1 升級(jí)到 2 時(shí)就執(zhí)行這個(gè)匿名類 Migration 中的邏輯
        private 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)")
            }
        }

        // 當(dāng)數(shù)據(jù)庫版本從 2 升級(jí)到 3 時(shí)執(zhí)行
        private 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 {
            instance?.let { return it }
            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 傳入 MIGRATION_1_2, MIGRATION_2_3
                .build().apply {
                    instance = this
                }
        }
    }
}

5. WorkManager

WorkManager 是一個(gè)處理定時(shí)任務(wù)的工具,它可以保證即使在應(yīng)用退出甚至手機(jī)重啟的情況下,之前注冊的任務(wù)仍然會(huì)得到執(zhí)行,因此它適合執(zhí)行一些定期和服務(wù)器進(jìn)行交互的任務(wù),如周期性同步數(shù)據(jù)等。

WorkManager 和 Service 并不相同,也沒直接的聯(lián)系,Service 是四大組件之一,在沒被銷毀時(shí)一直運(yùn)行在后臺(tái)。

另外,系統(tǒng)為了減少電量消耗,使用 WorkManager 注冊的周期性任務(wù)不能保證一定會(huì)準(zhǔn)時(shí)執(zhí)行,可能會(huì)將觸發(fā)時(shí)間臨近的幾個(gè)任務(wù)放在一起執(zhí)行。

5.1 WorkManager 的基本用法

要使用 WorkManager,需要在 build.gradle 中添加如下依賴:

implementation "androidx.work:work-runtime:2.3.4"

其基本用法主要有以下三步:

  • (1)定義一個(gè)后臺(tái)任務(wù),并實(shí)現(xiàn)具體的任務(wù)邏輯

  • (2)配置該后臺(tái)任務(wù)的運(yùn)行條件和約束信息,并構(gòu)建后臺(tái)任務(wù)請求

  • (3)將該后臺(tái)任務(wù)請求傳入 WorkManager 的enqueue()方法中,系統(tǒng)會(huì)在合適的時(shí)間運(yùn)行

下面舉個(gè)栗子,首先定義一個(gè)后臺(tái)任務(wù)如下:

// 每一個(gè)后臺(tái)任務(wù)都必須繼承自 Worker 類,并調(diào)用它唯一的構(gòu)造函數(shù),然后重寫 doWork() 方法
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    // doWork 方法不會(huì)運(yùn)行在主線程中,可處理耗時(shí)操作
    override fun doWork(): Result {
        Log.d("SimpleWorker", "do work in SimpleWorker")
        // 返回一個(gè) Result 對象,成功:Result.success() 失?。篟esult.failure() 或 Result.retry()
        return Result.success()
    }
}

接著配置運(yùn)行條件和約束信息:

// WorkRequest.Builder 有兩個(gè)子類:
// OneTimeWorkRequest.Builder:構(gòu)建單次運(yùn)行的后臺(tái)任務(wù)請求
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()

// PeriodicWorkRequest.Builder:構(gòu)建周期性運(yùn)行的后臺(tái)任務(wù)請求
// 如構(gòu)建運(yùn)行周期間隔不能短于15分鐘的后臺(tái)任務(wù)
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()

最后把構(gòu)建出的任務(wù)傳入 WorkManager 中:

WorkManager.getInstance(context).enqueue(request)

如在 Activity 中調(diào)用:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        doWorkButton.setOnClickListener {
            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
            WorkManager.getInstance(this).enqueue(request)
        }

    }
}

5.2 使用 WorkManager 處理復(fù)雜的任務(wù)

WorkManager 除了運(yùn)行時(shí)間外,還允許控制許多其他的東西,如下:

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    // 指定延遲時(shí)間后運(yùn)行
    .setInitialDelay(5, TimeUnit.MINUTES)
    // 添加標(biāo)簽
    .addTag("simple") 
    // 若任務(wù)返回了 Result.retry(),可以結(jié)合 setBackoffCriteria() 方法來重新執(zhí)行任務(wù) 
    // 第一個(gè)參數(shù)有兩種:LINER 重試時(shí)間以線性方式延遲,EXPONENTIAL 重試時(shí)間以指數(shù)方式延遲
    .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
    .build()
        
// 通過標(biāo)簽取消任務(wù)
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
// 通過 id 取消任務(wù)
WorkManager.getInstance(this).cancelWorkById(request.id)
// 取消全部任務(wù)
WorkManager.getInstance(this).cancelAllWork()

另外,可以調(diào)用 getWorkInfoByIdLiveData()getWorkInfoByTagLiveData() 方法來監(jiān)聽任務(wù)運(yùn)行結(jié)果,如:

WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this){
    if (it.state == WorkInfo.State.SUCCEEDED){
        Log.d("MainActivity", "do work success")
    } else if (it.state == WorkInfo.State.FAILED){
        Log.d("MainActivity", "do work failed")
    }
}

再介紹個(gè) WorkManager 中比較有特色的功能——鏈?zhǔn)饺蝿?wù)。

如定義了3個(gè)獨(dú)立后臺(tái)任務(wù):同步數(shù)據(jù)、壓縮數(shù)據(jù)、上傳數(shù)據(jù),要想先同步、再壓縮、最后上傳就可以借助鏈?zhǔn)饺蝿?wù)來實(shí)現(xiàn):

val sync = ...
val compress = ...
val upload = ...
WorkManger.getInstance(this)
    .beginWith(sync)
    .then(compress)
    .then(upload)
    .enqueue()

注:必須前一個(gè)任務(wù)成功后下一個(gè)任務(wù)才會(huì)運(yùn)行,若某個(gè)任務(wù)失敗或取消了,那么接下來的任務(wù)都不會(huì)運(yùn)行。

注:WorkManager 在國產(chǎn)手機(jī)上可能會(huì)非常不穩(wěn)定,不要依賴它去實(shí)現(xiàn)核心功能。

本篇文章就介紹到這。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容