1. 程式人生 > 其它 >學習Android之探究Jetpack

學習Android之探究Jetpack

高階程式開發元件——Jetpack

  Jetpack是一個開發元件工具集,它的主要目的是幫助我們編寫出更加簡潔的程式碼,並簡化我們的開發過程。Jetpack中的元件有一個特點,它們大部分不依賴於任何Android系統版本,這意味著這些元件通常是定義在AndroidX庫當中的,並且擁有非常好的向下相容性。

  Jetpack主要由基礎、架構、行為、介面這4部分組成。其實裡面有很多東西我們都是已經學過了的,比如通知、許可權、Fragment等等。

  其中的許多架構元件就是專門為MVVM架構量身打造的。 

1 ViewModel

   ViewModel算是Jetpack中最重要的元件之一了。在傳統開發模式下,Activity的任務很重,既要負責邏輯處理,還要控制UI展示,甚至還要處理網路回撥等。一旦在大專案中用這種方式,專案就會表的臃腫且難以維護,這就體現出了MVP、MVVM架構的重要性。

  而ViewModel的一個重要作用就是可以幫助Activity分擔一部分工作,它是專門用於存放與介面相關的資料的。可以在一定程度上減少Activity中的邏輯。

  此外,ViewModel還有一個重要特性,當手機發生橫豎屏旋轉時,Activity會重新建立,裡面的資料會丟失,而ViewModel不會被重新建立,只有當Activity退出時才會銷燬。因此,與介面相關的變數存放在ViewModel中,不用擔心發生旋轉時資料丟失。

  我們來看一下ViewModel的生命週期:

   

1.1 ViewModel的基本用法

  通過實現一個簡單的計數器來學習ViewModel的基本用法。

  Jetpack的元件通常是以AndroidX庫的形式釋出的,所以一些常用的Jetpack元件會在專案建立時自動包含進去。

  不過要用ViewModel元件還是需要新增依賴:

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

  一個好的程式設計規範是給每一個Activity和Fragment都建立一個對應的ViewModel,因此這裡需要為MainActivity建立一個對應的MainViewModel類,繼承自ViewModel,我們要實現的是一個計數器的功能,所以在這裡面定義一個counter變數計數:

class MainViewModel : ViewModel() {
    var counter = 0
}

  我們可以給介面新增一個按鈕,點選一次計數器就+1,並把最新的計數顯示出來,修改activity_main.xml中的程式碼:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <Button
        android:id="@+id/plusOneBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Plus One" />
</LinearLayout>

 

  接著開始實現計數的邏輯,回到MainActivity中:

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    
    lateinit var viewModel: MainViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        binding.plusOneBtn.setOnClickListener { 
            viewModel.counter++
            refreshCounter()
        }
     refreshCounter() }
private fun refreshCounter() { binding.infoText.text = viewModel.counter.toString() } }

 

   注意:不能直接去建立ViewModel的例項,一定要通過ViewModelProvider來獲取ViewModel的例項。語法如下:

        ViewModelProvider(<Activity或Fragment例項>).get(<ViewModel>::class.java)

 

   這是因為ViewModel有著獨立的生命週期,並且長於Activity,如果在onCreate()方法中建立ViewModel的例項,那麼每次執行onCreate()方法的時候,ViewModel都會建立一個新的例項,這樣就當旋轉時就無法保留資料了。

 

 1.2 向ViewModel傳遞引數

   我們發現,上面建立的MainViewModel的建構函式中沒有任何引數,但是如果我們需要通過建構函式來傳遞一些引數,該怎麼做?由於所有ViewModel的例項都是用過ViewModelProvider來獲取的,因此沒有任何地方可以向ViewModel的建構函式中傳遞引數。

  不過我們只需要藉助ViewModelProvider.Factory就可以實現了。

  現在在螢幕旋轉的時候不會丟失資料,但是退出程式再進來資料就會被清零,這裡就實現一下儲存資料功能。在退出程式時儲存計數,然後開啟時讀取儲存的計數,並傳遞給ViewModel,修改MainViewModel中的程式碼:

class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = countReserved
}

 

   這裡很好理解,接下來就是如何向MainViewModel的建構函式中傳遞資料了。新建一個MainViewModelFactory類,繼承ViewModelProvider.Factory介面:

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

 

  MainViewModelFactory的建構函式中也接收了一個countReserved引數,然後實現create()方法,在此方法中,建立了MainViewModel的例項,並將countReserved引數傳了進入。

  為什麼這裡可以直接建立MainViewModel的例項呢?因為create()方法的執行時機和Activity的生命週期無關,所以不會產生問題。

  另外,我們還可以新增一個清零按鈕,id為clearBtn。

  最後回到MainActivity中:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    lateinit var viewModel: MainViewModel
    lateinit var sp : SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        sp = getPreferences(Context.MODE_PRIVATE)

        val countReserved = sp.getInt("count_reserved", 0)
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved))
            .get(MainViewModel::class.java)

        ...
        binding.clearBtn.setOnClickListener {
            viewModel.counter = 0
            refreshCounter()
        }
        refreshCounter()

    }

    override fun onPause() {
        super.onPause()
        sp.edit {
            putInt("count_reserved", viewModel.counter)
        }
    }

    ...
}

 

 

  首先獲取SharedPreferences的例項,讀取之前儲存的資料。接著在ViewModelProvider中,額外傳入了一個MainViewModelFactory引數,將讀取的資料傳給了MainViewModelFactory的建構函式。只有這種寫法才能將資料最終傳遞給MainViewModel的建構函式。

  至此,傳參功能完成。

 

2 Lifecycles

  我們可能經常會遇到感知Activity生命週期的情況,比如,某介面中發起了一條網路請求,但是當請求得到迴應的時候,介面可能已經關閉了,此時就不應該繼續對響應結果進行處理。因此,我們需要能時刻感知到Activity的生命週期,以便在適當的時候進行邏輯控制。

  在一個Activity中去感知它的生命週期非常簡單,而如果要在一個非Activity的類中去感知Activity的生命週期,應該怎麼辦呢?

  可以通過在Activity中嵌入一個隱藏的Fragment來進行感知,或者通過手寫監聽器的方式來進行感知,等等。

手寫監聽器的方式

  通過手寫監聽器的方式來對Activity的生命週期進行感知:

class MyObserver {
    fun activityStart() {
    }
    fun activityStop() {
    }
}

class MainActivity : AppCompatActivity() {
    
    lateinit var observer: MyObserver
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        observer = MyObserver()
    }

    override fun onStart() {
        super.onStart()
        observer.activityStart()
    }

    override fun onStop() {
        super.onStop()
        observer.activityStop()
    }
}

 

  為了能讓MyObserver能夠感知到Activity的生命週期,需要專門在MainActivity中重寫響應的生命週期方法,然後再通知給MyObserver。這種方式雖然能夠正常工作,但是需要在Activity中編寫太多額外的邏輯。

  而Lifecycles元件就是為了解決這一問題而出現的,它可以讓任何一個類都能輕鬆感知到Activity的生命週期,同時也不需要大量編寫邏輯。

  使用它,先新建一個MyObserver類,實現LifecycleObserver介面:

class MyObserver : LifecycleObserver {
}

  LifecycleObserver是一個空方法介面,不需要重寫任何方法。

  接下來可以在裡面定義方法了,想要感知Activity的生命週期,還得藉助額外的註解功能才能,如下:

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: ")
    }
}

  在方法上使用了@OnLifecycleEvent註解,並傳入了一種生命週期事件。

  生命週期事件的型別有7種:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY分別匹配Activity中相應的生命週期回撥;還有一種ON_ANY表示可以匹配Activity的任何生命週期回撥。

  所以,上述程式碼中的activityStart()和activityStop()方法就應該分別在Activity的onStart()和onStop()觸發的時候執行。

  但是目前還是不能正常工作的,因為當Activity的生命週期發生變化時並沒有去通過MyObserver,這個時候就要藉助LifecycleOwner,它使用如下語法結構讓MyObserver得到通知:

        lifecycleOwner.lifecycle.addObserver(MyObserver())

  首先呼叫LifecycleOwner的getLifecycle()方法,得到一個Lifecycle物件,然後呼叫它的addObserver()方法觀察LifecycleOwner的生命週期,再把MyObserver的例項傳進去就可以了。

  接下來的問題就是,怎樣獲取LifecycleOwner的例項?

  實際上,如果Acitvity是繼承自AppCompatActivity的,或者Fragment是繼承自androidx.fragment.app.Fragment的,那麼它們本身就是一個LifecycleOwner的例項。這樣我們就可以在MainActivity中這樣寫:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycle.addObserver(MyObserver())
    }
}

  現在,MyObserver能夠自動感知Activity的生命週期了,上述內容在Fragment也是通用的。

  不過目前只是感知,我們還可以主動獲取當前的生命週期狀態。只需要在MyObserver的建構函式中講Lifecycle物件傳進來即可:

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

  有了Lifecycle物件,就可以在任何地方呼叫lifecycle.currentState來主動獲取當前的生命週期狀態。

  lifecycle.currentState返回的生命週期狀態是一個列舉型別,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED這5種狀態型別。它們與Activity的生命週期回撥所對應的關係如圖所示。

 

  當獲取的生命週期狀態是CREATED的時候,說明onCreate()方法已經執行了,但是onStart()方法還沒有執行。當獲取的生命週期狀態是STARTED的時候,說明onStart()方法已經執行了,但是onResume()方法還沒有執行,以此類推。

 

3 LiveData

 

   LiveData是Jetpack提供的一種響應式程式設計元件,它可以包含任何型別的資料,並在資料發生變化的時候通知給觀察者。LiveData特別適合與ViewModel結合在一起使用。

3.1 LiveData的基本用法

  回顧上面編寫的計數器其實是存在問題的,當每次點選+1按鈕的時候,都會先給ViewModel中的計數+1,然後立即獲取最新的計數。這種方式在單執行緒中可以正常工作,但是如果在ViewModel的內部開啟了執行緒去執行一些耗時邏輯,那麼在點選+1按鈕之後會立刻去獲取最新的資料,得到的肯定還是之前的資料。

  我們一直使用的都是在Activity中手動獲取ViewModel中的資料這種互動方式,但是ViewModel卻無法將資料的變化主動通知給Activity。不要想著把Activity的例項傳給ViewModel來實現主動通知,這是錯誤的做法,是很有可能造成記憶體洩漏的。

  解決方案就是使用LiveData,如果我們用LiveData來包裝計數器的計數,然後在Activity中觀察它,就可以主動將資料變化通知給Activity了。

  具體實現如下,修改MainViewModel中的程式碼:

class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = MutableLiveData<Int>()
    init {
        counter.value = countReserved
    }
    fun plusOne() {
        val count = counter.value ?: 0
        counter.value = count + 1
    }
    fun clear() {
        counter.value = 0
    }
}

 

  這裡的counter變數成為了一個MutableLiveData物件,泛型為Int。MutableLiveData是一種可變的LiveData,它有3中讀寫資料的方法,分別是getValue()、setValue()、postValue()方法。

  getValue()方法用於獲取LiveData中包含的資料;

  setValue()方法用於給LiveData設定資料,但是隻能在主執行緒中呼叫;

  postValue()方法用於在非主執行緒中給LiveData設定資料。

  plusOne()方法中取到的資料可能為空,所以使用了一個?:操作符,當獲取到空資料時,用0來作為預設計數。MainViewModel修改完了。

  接下來修改MainActivity:

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        binding.plusOneBtn.setOnClickListener {
            viewModel.plusOne()
        }
        binding.clearBtn.setOnClickListener {
            viewModel.clear()
        }
        viewModel.counter.observe(this, Observer { count ->
            binding.infoText.text = count.toString()
        })
    }

    override fun onPause() {
        super.onPause()
        sp.edit { 
            putInt("count_reserved", viewModel.counter.value ?: 0)
        }
    }
}

 

  在點選事件中我們呼叫的是MainViewModel中的相應方法。最關鍵的一步是,呼叫了viewModel.counter的observe()方法來觀察資料的變化。現在counter變數是一個LiveData物件,任何LiveData物件都可以呼叫它的observe()方法來觀察資料的變化。

  observe()方法接收兩個引數:

    第一個是一個LifecycleOwner物件,而Activity本身就是一個LifecycleOwner物件。

    第二個是一個Observer介面,當counter中包含的資料發生變化時,就會回撥到這裡,因此在這裡將計數更新。

  這個時候就不用擔心ViewModel內部會不會開啟執行緒執行耗時邏輯了。如果需要在子執行緒中給LiveData設定資料,一定要使用postValue()方法。

 

  這裡思考一個問題?

  LiveData的observe()方法是一個Java方法,觀察Observer介面,會發現這是一個單抽象方法介面,只有一個待實現的onChanged()方法。既然是單抽象方法介面,為什麼呼叫observe()方法時卻沒有使用之前學過的Java函式式API的寫法呢?

  這種情況比較特殊,因為observe()方法接收的另一個引數LifecycleOwner也是一個單抽象方法介面。當一個Java方法同時接收兩個單抽象方法介面引數時,要麼同時使用函式式API的寫法,要麼都不使用函式式API的寫法。因為我們傳入的第一個引數是this,所以第二個引數就不能使用函式式API的寫法了。

 

  不過,有一個專門為Kotlin語言設計的庫——lifecycle-livedata-ktx,這個庫在2.2.0版本加入了對observe()方法的語法擴充套件,只需要新增一下依賴:

    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'

 

  然後就可以使用如下語法結構的observe()方法:

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

  現在是能正常工作的,但是並不規範,主要問題就是將counter這個可變的LiveData暴露給了外部,破壞了ViewModel資料的封裝性,還有一定風險。

  推薦做法是:永遠只暴露不可變的LiveData給外部。這樣在非ViewModel中只能觀察到LiveData的資料變化,而不能給LiveData設定資料。修改MainViewModel來實現這樣的功能:

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

  這裡先將原來的counter變數改名為_counter變數, 並加上private修飾符,這樣_counter變數對於外部就是不可見的了。然後又新定義了一個counter變數,將它的型別宣告為不可變的LiveData,並在它的get()屬性方法中返回_counter變數。

  這樣,當外部呼叫counter變數時,實際上獲取到的是_counter的例項,但是無法給counter設定資料,從而保證了ViewModel的資料封裝性。

  目前這種寫法是非常規範的,也是官方比較推薦的。

 

3.2 map和switchMap

  LiveData提供了兩種轉換方法:map()和switchMap()方法。

  map()方法的作用是將實際包含函式的LivaData和僅用於觀察資料的LiveData進行轉換。比如說有一個User類,其中包含使用者的姓名和年齡,我們可以在ViewModel中建立一個相應的LiveData來包含User型別的資料,如下:

class MainViewModel(countReserved: Int) : ViewModel() {
    val userLiveData = MutableLiveData<User>()
    ...
}

  此時如果明確MainActivity中只會顯示使用者的姓名,而不在意年齡。那麼這個時候還將整個User型別的LiveData暴露給外部就不合適了。

  而map()方法就是專門用來解決這種問題的。它可以將User型別的LiveData自由地轉型成任意其他型別的LiveData,用法如下:

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

    private val userLiveData = MutableLiveData<User>()

    val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
        "${user.firstName} ${user.lastName}"
    }
    ...
}

  這裡呼叫了Transformations的map()方法來對LiveData的資料型別進行轉換。

  map()接收兩個引數:

    第一個是原始的LiveData物件;

    第二個是一個轉換函式,在裡面編寫具體的轉換邏輯即可。這裡就是將User物件轉換成了一個只包含使用者姓名的字串。

  現在userLiveData宣告成了private,保證了資料的封裝性,外部使用的時候就觀察userName這個LiveData即可。

  當userLiveData的資料發生變化時,map()方法會監聽到變化並執行轉換函式中的邏輯,然後再將轉換之後的資料通知給userName的觀察者。

 

  而switchMap()方法使用場景非常固定,但可能比map()更常用。

  我們之前所有的LiveData物件的例項都是在ViewModel中建立的。在實際的專案中,很可能ViewModel中的某個LiveData物件是呼叫另外的方法獲取的。

  比如以下情況,新建一個Repository單例類,如下所示:

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

 

  這裡將每次傳入的userId當作使用者姓名來建立一個新的User物件。

  每次呼叫getUser()方法都會返回一個新的LiveData例項。

  然後我們在MainViewModel中也定義一個getUser()方法,並呼叫Repository的getUser()方法來獲取LiveData物件:

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

 

  接下來就是解決如何在Activity中觀察LiveData的資料變化。

  上面提到過 “每次呼叫getUser()方法都會返回一個新的LiveData例項。”,所以如果使用一下寫法是錯誤的:

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

  因為每次得到的都是一個新的LiveData例項,無法觀察到資料的變化。

  此時就出現了switchMap()方法,它的使用場景很固定:如果ViewModel中的某個LiveData物件是呼叫另外的方法獲取的,那麼我們就可以藉助switchMap()方法,將這個LiveData物件轉換成另外一個可觀察的LiveData物件。

  回到MainViewModel中:

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

 

  這裡定義了一個新的userIdLiveData物件,來觀察userId的資料變化,然後呼叫Transformations的switchMap()方法,用來對另一個可觀察的LiveData物件進行轉換。

  swiychMap()方法接收兩個引數:

    第一個是新增的userIdLiveData,switchMap()方法會對它進行觀察;

    第二個是一個轉換函式。還必須在這個轉換函式中返回一個LiveData物件,因為switchMap()方法的工作原理就是要將轉換函式中返回的LiveData物件轉換成另一個可觀察的LiveData物件。所以我們只需要在轉換函式中呼叫Repository的getUser()方法來得到LiveData物件並返回。

 

 

  來梳理一遍switchMap()的工作流程:

  首先,當外部呼叫MainViewModel的getUser()方法來獲取使用者資料時,並不會發起任何請求或者函式呼叫,只會將傳入的userId值設定到userIdLiveData當中。一旦userIdLiveData的資料發生變化,那麼觀察userIdLiveData的switchMap()方法就會執行,並且呼叫我們編寫的轉換函式。然後在轉換函式中呼叫Repository.getUser()方法獲取真正的使用者資料。同時,switchMap()方法會將Repository.getUser()方法返回的LiveData物件轉換成一個可觀察的LiveData物件,對於Activity而言,只要去觀察這個LiveData物件就可以了。

  接下來我們來了解一下LiveData、ViewModel和Lifecyclers元件之間的關係。

  LiveData之所以能夠成為Activity和ViewModel之間通訊的橋樑,並且還不會有記憶體洩漏的風險,靠的就是Lifecyclers元件。LiveData在內部使用了Lifecyclers元件來自我感知生命週期的變化,從而可以在Activity銷燬的時候及時釋放引用,避免產生記憶體洩漏的問題。

  另外,由於要減少效能消耗,當Activity處於不可見的狀態的時候(比如手機息屏,或者被其他的Activity遮擋),如果LiveData中的資料發生了變化,是不會通知給觀察者的。只有當Activity重新恢復可見狀態時,才會將資料通知給觀察者,而LiveData之所以能夠實現這種細節的優化,依靠的還是Lifecyclers元件。

  如果在Activity處於不可見狀態的時候,LiveData發生了多次資料變化,當Activity恢復可見狀態時,只有最新的那份資料才會通知給觀察者,前面的資料在這種情況下相當於已經過期了,會被直接丟棄。

 

4 Room

  市面上有許多專門為Android資料庫設計的ORM框架。ORM(Object Relational Mapping)也叫物件關係對映。我們用的程式語言是面嚮物件語言,而用的資料庫是關係型資料庫,將面向物件的語言和麵向關係的資料庫之間建立一種對映關係,這就是ORM了。

  它給我們帶來的好處就是可以使用面向物件的思維來和資料庫互動。而Android官方推出的一個ORM框架就是Room。

4.1 使用Room增刪改查

  Room的整體結構主要由Entity、Dao和Database三部分組成。

  • Entity:用於定義封裝實際資料的實體類,每個實體類都會在資料庫中有一張對應的表,並且表中的列是根據實體類中的欄位自動生成的。
  • Dao:Dao是資料訪問物件的意思,通常會在這裡對資料庫的各項操作進行封裝,在實際程式設計的時候,邏輯層就不需要和底層資料庫打交道了,直接和Dao層進行互動即可。
  • Database:用於定義資料庫中的關鍵資訊,包括資料庫的版本號、包含哪些實體類以及提供Dao層的訪問例項。

   使用Room還需要新增依賴:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
dependencies {
    ...
    implementation 'androidx.room:room-runtime:2.4.2'
    kapt 'androidx.room:room-compiler:2.4.2'
}

 

   這裡新增了一個kotlin-kapt外掛,同時在dependencies閉包中添加了兩個Room的依賴庫。由於Room會根據我們在專案中宣告的註解來動態生成程式碼,因此這裡一定要使用kapt引入Room的編譯時註解庫,而啟用編譯時註解功能則一定要先新增kotlin-kapt外掛。注意,kapt只能在kotlin專案中使用,如果是Java專案,就要使用annotationProcessor。

 

  首先來定義實體類,可以用之前定義的User類,但是還需要進行修改,如下:

@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

  一個良好的資料庫程式設計習慣是給每個實體類都加上一個id欄位,並設定為主鍵。

 

  接下來是定義Dao,這是Room中最關鍵的地方,所有訪問資料庫的操作都在這兒。新建一個UserDao介面:

@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
}

 

   介面上面要使用@Dao註解,這樣Room才能將它識別成一個Dao。Room提供了@Insert、@Delete、@Update和@Query這4種相應的註解。@Insert註解插入資料後會返回主鍵id值。

  如果想要從資料庫中查詢資料或者使用非實體類引數來增刪改資料,那麼就必須編寫SQL語句了。就比如loadUsersOlderThan()方法和deleteUserByLastName()方法,需要在@Query註解中編寫SQL語句進行增刪改。

  而且Room是支援在編譯時動態檢查SQL語句的。

 

  最後定義Database,它只有3個部分的內容:資料庫版本號、包含的實體類、Dao層的訪問例項。新建AppDatabase.kt檔案:

@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
                }
        }
    }
}

 

  @Database註解中多個實體類之間用逗號隔開,AppDatabase類必須繼承自RoomDatabase類,並使用abstract關鍵字將它宣告成抽象類,然後提供相應的抽象方法,用於獲取之前編寫的Dao的例項,比如這裡提供的userDao()方法。不過我們只需要進行方法宣告就可以了,具體的方法實現是由Room在底層自動完成的。

  然後在companion object結構體中編寫了一個單例模式,因為原則上全域性應該只存在一份AppDatabase的例項。然後在getDatabase()方法中判斷:如果instance變數不為空就直接返回,否則就呼叫Room.databaseBuilder()方法來構建一個AppDatabase的例項。

  databaseBuilder()方法接收3個引數,注意第一個引數一定要使用applicationContext,而不能使用普通的context,否則容易出現記憶體洩漏的情況。第二個引數是AppDatabase的Class型別,第三個引數是資料庫名。最後呼叫build()方法完成構建,並將創建出來的例項賦值給instance變數,然後返回當前例項即可。

 

  在Activity中使用方法如下:

        val userDao = AppDatabase.getDatabase(this).userDao()
        val user1 = User("john","man", 25)
        binding.addDataBtn.setOnClickListener {
            thread {
                user1.id = userDao.insertUser(user1)
            }
        }
        binding.queryDataBtn.setOnClickListener {
            thread {
                for (user in userDao.loadAllUsers()) {
                    Log.d("MainActivity", user.toString())
                }
            }
        }

 

  很容易理解,先獲取UserDao的例項,再呼叫相應方法即可。由於資料庫操作屬於耗時操作,Room預設是不允許在主執行緒中進行資料庫操作的,所以開啟了子執行緒。

  不過為了方便測試,可以在構建AppDatabase例項的時候,接入一個allowMainThreadQueries()方法,這樣Room就允許在主執行緒中進行資料庫操作了,不過只建議在測試環境下使用。如下:

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

 

4.2 Room的資料庫升級

  Room資料庫的升級還是不太簡便,如果專案還在開發測試階段,那麼可以使用如下方法:

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

  這個方法會將當前資料銷燬後重建,副作用就是裡面的資料都會消失。

  已經發布的產品就不適合用這種方式了,標準方式如下:

  1.   如果需要在資料庫中新增一張表Book,首先就是建立Book的實體類,類中包含了主鍵id、書名、頁數字段;
  2.   然後建立一個BookDao介面,在裡面定義一些API;
  3.   最後修改AppDatabase中的程式碼。程式設計資料庫升級邏輯,如下:
@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
        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,
            "app_database")
                .addMigrations(MIGRATION_1_2)
                .build().apply {
                    instance = this
                }
        }
    }
}

 

  首先在第一行註解中升級版本號,並加入Book實體類,接著提供bookDao()方法獲取例項。

  關鍵點在於:實現了一個匿名類Migration,它的例項變數命名為MIGRATION_1_2,傳入1和2,表示資料庫版本從1升級到2的時候就執行匿名類中的升級邏輯。之後在裡面編寫相應的SQL語句。

  最後構建AppDatabase例項的時候,加入addMigrations方法,並傳入MIGRATION_1_2即可。

 

  如果是資料庫升級是需要向表中新增列的話,就用alter語句修改表結構即可,比如現在我們往Book表中新增一個作者欄位,首先修改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 = [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'")
            }
        }

        ...return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,
            "app_database")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .build().apply {
                    instance = this
                }
        }
    }
}

 

 

5 WorkManager

  Android 8.0系統開始禁用了Service的後臺功能,只允許使用前臺Service。

  WorkManager很適合用於處理一些要求定時執行的任務,它可以根據作業系統的版本自動選擇底層的實現方法,降低我們的使用成本,它還支援週期性任務、鏈式任務處理等。

  但是WorkManager和Service沒有直接的聯絡。Service是四大元件之一,在沒有被銷燬的情況下一直在後臺執行,而WorkManager是一個處理定時任務的工具,它可以保證即使在應用退出甚至手機重啟的情況下,之前註冊的任務依然能得到執行,因此WorkManager很適合用於執行一些定期和伺服器進行互動的任務,比如週期性同步資料等。

5.1 WorkManager的基本用法

  新增依賴:

    implementation 'androidx.work:work-runtime:2.7.1'

  它的基本用法分為3步:

  1. 定義一個後臺任務,實現具體邏輯;
  2. 配置該後臺任務的執行條件和約束資訊,並構建後臺任務請求;
  3. 將該後臺任務請求傳入WorkManager的enqueue()方法中,系統會在適合的時候執行。

  

  第一步,定義一個後臺任務,建立一個SimpleWorker類:

class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
      // 編寫具體後臺任務邏輯 Log.d(
"SimpleWorker", "doWork: ") return Result.success() } }

  繼承自Worker類,並呼叫它唯一的建構函式,然後重寫它的doWork()方法。

  doWork()方法不會執行在主執行緒中,可以在裡面執行耗時操作。返回的Resule物件表示任務執行的結果。

 

  第二步,進行最基本的配置:

        val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).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(context).enqueue(request)

 

 

5.2 使用WorkManager處理複雜任務

  我們可以讓後臺任務在指定的延遲時間後執行,藉助setIntitalDelay()方法:

        val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
            .setInitialDelay(5, TimeUnit.MINUTES)
            .build()

  這裡是5分鐘後執行,可以自行指定時間單位。

  還可以給後臺任務新增標籤:

        val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
            .setInitialDelay(5, TimeUnit.MINUTES)
            .addTag("simple")
            .build()

 

  新增標籤的好處就是可以通過標籤來取消後臺任務請求:

        WorkManager.getInstance(this).cancelAllWorkByTag("simple")

  也可以使用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個引數:

    第一個引數用於指定如果任務再次執行失敗,下次重試的時間以什麼樣的形式延遲,有兩個值可選,LINEAR表達以線性的方式延遲,EXPONENTIAL表示以指數的方式延遲。

    第二和第三個引數就很好理解了,重新執行任務的時間不能少於10秒。

  這就是Result.retry()的作用,而Result.success()和Result.failure()的作用就是通知任務執行結果的,我們可以對執行結果監聽:

        WorkManager.getInstance(this)
            .getWorkInfoByIdLiveData(request.id)
            .observe(this) { workInfo ->
                if (workInfo.state == WorkInfo.State.SUCCEEDED) {
                    Log.d("MainActivity", "suceeded ")
                } else if (workInfo.state == WorkInfo.State.FAILED) {
                    Log.d("MainActivity", "failed ")
                }
            }    

  呼叫getWorkInfoByIdLiveData()方法,並傳入後臺任務請求的id,會返回一個LiveData物件。然後就可以呼叫LiveData物件的observe()方法來觀察資料變化了,以此監聽後臺任務的執行結果。

 

鏈式任務

  比如定義了3個獨立的後臺任務:同步資料、壓縮資料和上傳資料。現在我們想要實現先同步、再壓縮、最後上傳的功能,就可以藉助鏈式任務來實現,程式碼如下:

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

  beginWith()方法用於開啟一個鏈式任務,後面要接上什麼樣的後臺任務,只需要使用then()方法來連線即可。另外WorkManager還要求,必須在前一個後臺任務執行成功之後,下一個後臺任務才會執行。也就是說,如果某個後臺任務執行失敗,或者被取消了,那麼接下來的後臺任務就都得不到運行了。

  不要依賴WorkManager去實現核心功能,因為在國產手機上可能不穩定,大多數國產手機廠商在進行Android系統定製的時候增加了一個一鍵關閉的功能,會殺死所有非白名單的應用程式,被殺死的應用程式及無法接受廣播,也無法執行WorkManager的後臺任務。