學習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()
這個方法會將當前資料銷燬後重建,副作用就是裡面的資料都會消失。
已經發布的產品就不適合用這種方式了,標準方式如下:
- 如果需要在資料庫中新增一張表Book,首先就是建立Book的實體類,類中包含了主鍵id、書名、頁數字段;
- 然後建立一個BookDao介面,在裡面定義一些API;
- 最後修改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步:
- 定義一個後臺任務,實現具體邏輯;
- 配置該後臺任務的執行條件和約束資訊,並構建後臺任務請求;
- 將該後臺任務請求傳入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的後臺任務。