Jetpack元件 DataStore的使用和簡單封裝
- 前言
- 正文
- 一、添加依賴
- 二、資料存取
- 三、資料檢視和清除
- 四、封裝
- 五、對象存取
- 1. 插件安裝
- ① 添加協定緩沖區插件
- ② 添加協定緩沖區和 Proto DataStore 依賴項
- ③ 配置協定緩沖區
- 2. 建立proto檔案
- 3. 配置proto檔案
- 4. 建立序列化器
- 5. 對象寫入和取出
- 六、源碼
前言
也許你是第一次聽說這個DataStore,也許你有所耳聞,但從未使用過,不過都沒有關系,随着這篇文章去熟悉DataStore。
正文
DataStore是Jetpack中的一個元件,用于做資料持久化,DataStore以異步、一緻的事務方式存儲資料,克服了SharedPreferences的一些缺點,DataStore基于Kotlin協程和Flow實作,就是用來取代SharedPreferences的。我們廢話不多說,開始吧。按照慣例,我們建立一個項目去做示範,不過稍微有一些不同,這次我們建立的項目時Kotlin語言的,請注意。
建立好項目,待項目配置完成之後,我們添加依賴。
一、添加依賴
在app子產品下的build.gradle中的dependencies{}閉包中添加如下依賴:
//DataStore
implementation 'androidx.datastore:datastore-preferences:1.0.0'
implementation 'androidx.datastore:datastore-preferences-core:1.0.0'
同時添加開啟ViewBinding和DataBinding,如下圖所示:
然後Sync Now。
二、資料存取
首先我們改一下activity_main.xml布局,裡面的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/btn_put"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="存資料" />
<Button
android:id="@+id/btn_get"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="取資料" />
</LinearLayout>
裡面就是兩個按鈕一個文本,回到MainActivity中,首先完成點選事件的監聽。
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//存資料
binding.btnPut.setOnClickListener {
}
//取資料
binding.btnGet.setOnClickListener {
}
}
這應該沒啥是好說的,就是使用了viewBinding,擷取視圖xml的控件id。
下面就是正式來使用DataStore了,首先我們需要定義一個變量。
//定義dataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "Study")
這裡的變量就是dataStore,我們在定義的時候給了一個Study的名稱,就像你使用SP時需要先給一個名字一樣,然後才是鍵值的操作。
在DataStore中操作資料會麻煩一些,Key需要我們去定義,例如我定義一個String類型的key。
//定義要操作的key
private val key = stringPreferencesKey("name")
這就是定義String類型的Key,通過這個Key去進行資料存取,還有一些其他的方法可供你使用。
基本上滿足你的要求,SP的功能它肯定都會有的,這裡這些方法可以快速建構一個符合類型的Key。
下面我們寫一個方法進行存資料,代碼如下:
private suspend fun put() = dataStore.edit { it[key] = "疫情" }
這裡用到了Kotlin的協程,如果你對這個不太了解,那麼也沒有關系,你先知道這麼用,然後再去了解協程。這個方法這樣不太清晰,換種方式:
通過dataStore.edit函數,裡面的it就是MutablePreferences,然後我們通過key去設定它的值,這裡是設定疫情兩個字。而這個suspend是協程中的關鍵字,你現在可以将這個put()當成是在子線程中執行的,那麼執行結束之後需要怎麼做呢?需要切換到主線程。這是在調用的地方進行切換,比如我們在點選存資料按鈕的時候調用,如下圖所示:
就是這樣的。
下面我們再寫一個取資料的方法。
private fun get() = runBlocking {
return@runBlocking dataStore.data.map { it[key] ?: "新冠" }.first()
}
你會發現和存資料又有不同,這裡的first()就是取值,這個方法換個方式來看就清晰一些。
然後我們在取資料按鈕的點選事件中調用。
下面我們運作一下:
第一次我先取資料,顯示的是預設值,然後我存資料再取資料。效果就是這樣,但你會覺得使用起來很麻煩,不如SP好用,這個我們後面再去封裝,先了解一些它的功能特性。
三、資料檢視和清除
在進行定義dataStore時,會在手機中生成一個pb檔案,這裡我們用虛拟機來看,
然後通過你的程式包名去找
這裡的檔案就是存放你的緩存資訊的檔案。這裡我用txt打開看一下
可以看到鍵和值,也許是浏覽檔案不對,下面我們清理一下這個資料。在布局中增加一個按鈕
在代碼中
通過clear方法調用進行資料的清除,清除後我們再看看這個pb檔案
這個檔案就什麼都沒有了,清除的幹幹淨淨。
四、封裝
這個DataStore是肯定需要封裝之後再使用的,直接使用太麻煩了,我們需要封裝的像SP那樣好用,資料類型就參考這個方法中的資料類型。
在寫封裝代碼之前呢,我們先建立一個App類,裡面的代碼如下:
class App : Application() {
companion object {
lateinit var instance : App
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
然後我們在AndroidManifest中設定
下面我們建立一個EasyDataStore類,将它設定為object,先建立DataStore,代碼如下:
// 建立DataStore
val App.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "Study"
)
// DataStore變量
val dataStore = App.instance.dataStore
下面我們先寫好各個資料類型的存取方法,先寫存資料的方法:
/**
* 存放Int資料
*/
private suspend fun putIntData(key: String, value: Int) = dataStore.edit {
it[intPreferencesKey(key)] = value
}
/**
* 存放Long資料
*/
private suspend fun putLongData(key: String, value: Long) = dataStore.edit {
it[longPreferencesKey(key)] = value
}
/**
* 存放String資料
*/
private suspend fun putStringData(key: String, value: String) = dataStore.edit {
it[stringPreferencesKey(key)] = value
}
/**
* 存放Boolean資料
*/
private suspend fun putBooleanData(key: String, value: Boolean) = dataStore.edit {
it[booleanPreferencesKey(key)] = value
}
/**
* 存放Float資料
*/
private suspend fun putFloatData(key: String, value: Float) = dataStore.edit {
it[floatPreferencesKey(key)] = value
}
/**
* 存放Double資料
*/
private suspend fun putDoubleData(key: String, value: Double) = dataStore.edit {
it[doublePreferencesKey(key)] = value
}
然後是取資料的方法:
/**
* 取出Int資料
*/
private fun getIntData(key: String, default: Int = 0): Int = runBlocking {
return@runBlocking dataStore.data.map {
it[intPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Long資料
*/
private fun getLongData(key: String, default: Long = 0): Long = runBlocking {
return@runBlocking dataStore.data.map {
it[longPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出String資料
*/
private fun getStringData(key: String, default: String? = null): String = runBlocking {
return@runBlocking dataStore.data.map {
it[stringPreferencesKey(key)] ?: default
}.first()!!
}
/**
* 取出Boolean資料
*/
private fun getBooleanData(key: String, default: Boolean = false): Boolean = runBlocking {
return@runBlocking dataStore.data.map {
it[booleanPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Float資料
*/
private fun getFloatData(key: String, default: Float = 0.0f): Float = runBlocking {
return@runBlocking dataStore.data.map {
it[floatPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Double資料
*/
private fun getDoubleData(key: String, default: Double = 0.00): Double = runBlocking {
return@runBlocking dataStore.data.map {
it[doublePreferencesKey(key)] ?: default
}.first()
}
最後我們根據存取的資料類型去做一個封裝,存資料,代碼如下:
/**
* 存資料
*/
fun <T> putData(key: String, value: T) {
runBlocking {
when (value) {
is Int -> putIntData(key, value)
is Long -> putLongData(key, value)
is String -> putStringData(key, value)
is Boolean -> putBooleanData(key, value)
is Float -> putFloatData(key, value)
is Double -> putDoubleData(key, value)
else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
}
}
}
取資料:
/**
* 取資料
*/
fun <T> getData(key: String, defaultValue: T): T {
val data = when (defaultValue) {
is Int -> getIntData(key, defaultValue)
is Long -> getLongData(key, defaultValue)
is String -> getStringData(key, defaultValue)
is Boolean -> getBooleanData(key, defaultValue)
is Float -> getFloatData(key, defaultValue)
is Double -> getDoubleData(key, defaultValue)
else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
}
return data as T
}
對了,還有一個清除資料的方法:
/**
* 清空資料
*/
fun clearData() = runBlocking { dataStore.edit { it.clear() } }
這樣我們的DataStore就封裝好了,下面我們在MainActivity中使用一下:
這裡我們存資料、取資料、清空資料都用到了,下面運作一下:
對于DataStore最基本的操作就完成了,那麼下面來進階一下。
五、對象存取
其實我們剛才使用的是Preferences DataStore,是對資料進行操作,下面要操作的是Proto DataStore,官網上的說法是Proto DataStore 将資料作為自定義資料類型的執行個體進行存儲。此實作要求您使用協定緩沖區來定義架構,但可以確定類型安全。
Proto DataStore中采用的是ProtorBuffer,優勢是性能好、效率高,表現在對資料的序列化和反序列化時間快,占用的空間小,還記得之前我們看到的那個pb檔案嗎,它裡面采用的就是protobuf,之前一直是Google内部使用,這也是源于它的缺點,之前這個pb檔案我們打開過,裡面隻能看懂鍵和值,缺乏描述,是以就影響了可讀性,和廣泛性,不如Json和XML簡單。是以我們目前也隻是在DataStore中使用protobuf,下面為了使用,我們需要在項目中裝一個插件。
1. 插件安裝
這個插件的安裝比較的麻煩,首先是添加協定緩沖區插件
① 添加協定緩沖區插件
首先打開工程的build.gradle,在裡面添加如下代碼:
id "com.google.protobuf" version "0.8.12" apply false
再打開app下的build.gradle,添加如下代碼:
'com.google.protobuf'
② 添加協定緩沖區和 Proto DataStore 依賴項
在app的dependencies{}閉包中添加如下代碼:
//Proto DataStore
implementation 'androidx.datastore:datastore-core:1.0.0'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
③ 配置協定緩沖區
在app的build.gradle中添加如下代碼:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
// 為該項目中的 Protobufs 生成 java Protobuf-lite 代碼。
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
注意它添加的位置:
點選Sync Now。
2. 建立proto檔案
将項目切換到Project,然後在main下面建立一個proto檔案夾。
在此檔案夾下建立study.proto檔案,然後AS會發現打開這個格式需要安裝一個插件。
點選Install plugins進行安裝。
安裝成功之後,重新開機AS插件生效。
注意看這個檔案的圖示變了,這說明你的插件安裝成功并且配置成功了。
3. 配置proto檔案
裡面的代碼如下:
// 聲明協定, 也支援 prota2,普遍使用proto3
syntax = "proto3";
/**
* 通過potorbuf 描述對象生成java類。
*/
option java_package = "com.llw.datastore";//設定生成的類所在的包
option java_multiple_files = true;//可能會有多個檔案。
message PersonPreferences {
string name = 1;
int32 age = 2;
}
這裡要按照Protobuf的語言規則去設定,參考protobuf 語言指南
這裡我們定了一個對象,然後你可以Make Project,此時通過編譯時技術,會生成一個PersonPreferences類,下面我們建立一個序列化器。
4. 建立序列化器
在com.llw.datastore下建立一個data包,包下建立一個PersonSerializer的單例,裡面的代碼如下:
object PersonSerializer : Serializer<PersonPreferences> {
override val defaultValue: PersonPreferences = PersonPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): PersonPreferences {
try {
return PersonPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: PersonPreferences, output: OutputStream) = t.writeTo(output)
}
這裡要注意導包的問題,這個類的作用就是PersonPreferences的序列化和反序列化。
5. 對象寫入和取出
這裡我們現在xml中增加兩個按鈕,如下所示:
然後回到MainActivity中,在裡面添加如下代碼:
//建立 DataStore
val Context.studyDataStore: DataStore<PersonPreferences> by dataStore(
fileName = "study.pb",
serializer = PersonSerializer
)
這裡就用到了那個序列化器,然後會儲存到study.pb下,下面來看存和取的方法,代碼如下:
//proto 存資料
binding.btnProtoPut.setOnClickListener {
runBlocking {
studyDataStore.updateData {
it.toBuilder()
.setName("劉愛國")
.setAge(11)
.build()
}
}
}
//proto 取資料
binding.btnProtoGet.setOnClickListener {
runBlocking {
val person = studyDataStore.data.first()
binding.textView.text = "name: ${person.name} , age: ${person.age}"
}
}