[TOC]
# 1. 前言
`Jetpack DataStore` 是一種數據存儲解決方案,允許您使用[協議緩沖區](https://developers.google.cn/protocol-buffers)存儲鍵值對或類型化對象。`DataStore `使用` Kotlin` 協程和` Flow` 以異步、一致的事務方式存儲數據。如果您當前在使用`SharedPreferences`存儲數據,請考慮遷移到 `DataStore`。如果您需要支持大型或復雜數據集、部分更新或參照完整性,請考慮使用[Room](https://developer.android.google.cn/training/data-storage/room),而不是 `DataStore`。
# 2. 分類
`DataStore` 提供兩種不同的實現:`Preferences DataStore `和 `Proto DataStore`。
* **Preferences DataStore**使用鍵值對存儲和訪問數據。此實現不需要預定義的架構,也不確保類型安全。`DataStore` 是基于 `Flow` 實現的,不會阻塞主線程。只支持`Int`,`Long`,`Boolean`,`Float`,`String`鍵值對數據,適合存儲簡單、小型的數據,并且**不支持局部更新**,如果修改了其中一個值,整個文件內容將會被重新序列化。
* **Proto DataStore**將數據作為自定義數據類型的實例進行存儲。此實現要求您使用**協議緩沖區**來定義架構,但可以確保類型安全。 **Proto DataStore**使用[協議緩沖區](https://developers.google.cn/protocol-buffers?hl=zh_cn)來定義架構。使用協議緩沖區可**持久保留強類型數據**。與 XML 和其他類似的數據格式相比,協議緩沖區速度更快、規格更小、使用更簡單,并且更清楚明了。雖然使用 `Proto DataStore` 需要學習新的序列化機制,但我們認為 `Proto DataStore` 有著強大的優勢,值得學習。
## 2.1 Preferences DataStore 和SharedPreferences的區別
* `SharedPreferences` 有一個看上去可以在界面線程中安全調用的同步` API`,但是該 `API` 實際上執行磁盤 `I/O` 操作。此外,`apply()`會阻斷`fsync()`上的界面線程。每次有服務啟動或停止以及每次 `activity` 在應用中的任何地方啟動或停止時,系統都會觸發待處理的`fsync()`調用。界面線程在`apply()`調度的待處理`fsync()`調用上會被阻斷,這通常會導致[ANR](https://developer.android.google.cn/topic/performance/vitals/anr?hl=zh_cn)。
# 3. 實踐
## 3.1 Preferences DataStore
### 3.1.1 依賴
~~~
// DataStore Preferences
implementation("androidx.datastore:datastore-preferences:1.0.0")
// optional - RxJava2 support
implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0")
// optional - RxJava3 support
implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0")
~~~
### 3.1.2 案例
下面案例來源于官網案例,地址:[DataStore ?|? Android 開發者 ?|? Android Developers (google.cn)](https://developer.android.google.cn/topic/libraries/architecture/datastore#kotlin)。
~~~
class MainActivity2 : AppCompatActivity() {
// 創建一個DataStore,并申明在頂層以方便調用
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
// 聲明一個int類型的key
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
private val textView: TextView by lazy { findViewById(R.id.text) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// 兩個按鈕的監聽事件
fun buttonOnClick(view: View){
runBlocking {
launch {
when(view.id){
R.id.update -> {
incrementCounter()
}
R.id.read -> {
textView.text = getData().toString()
}
}
}
}
}
// 存儲-自增1
suspend fun incrementCounter() {
dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
// 獲取當前值
suspend fun getData(): Int {
return dataStore.data.map { settings ->
settings[EXAMPLE_COUNTER] ?: 0
}.first()
}
}
~~~
結果:

### 3.1.3 最后
在前面提到了,這種類型的`Preferences DataStore`還支持的數據類型有:`Long`,`Boolean`,`Float`,`String`等,不妨看看對應的聲明函數:
~~~
longPreferencesKey()
booleanPreferencesKey()
floatPreferencesKey()
stringSetPreferencesKey()
~~~
對應的,隨便找一個方法,看下是如何實現的:
~~~
@JvmName("longKey")
public fun longPreferencesKey(name: String): Preferences.Key<Long> = Preferences.Key(name)
~~~
也就是其實底層也還是使用`Preferences.Key<Long>`來指明類型。和`SP`類似數據內容也存儲在本地磁盤`data/data/packagename/`中:

## 3.2 Proto DataStore
對于`Preferences DataStore `中的鍵只能為上述指定的類型,故而如果我們需要存儲自定義對象的數據的時候,就顯得力不從心了。故而在`jetpack`中提供了`Proto DataStore`。
### 3.2.1 依賴
引入`datastore`的依賴:
~~~
implementation("androidx.datastore:datastore:1.0.0")
// optional - RxJava2 support
implementation("androidx.datastore:datastore-rxjava2:1.0.0")
// optional - RxJava3 support
implementation("androidx.datastore:datastore-rxjava3:1.0.0")
~~~
為了使用 `Proto DataStore`,讓協議緩沖區為我們的架構生成代碼,我們需要對 `build.gradle` 文件進行一些更改:
#### 3.2.1.1 添加協議緩沖區插件
~~~
plugins {? ?
...? ?
id "com.google.protobuf" version "0.8.12"
}
~~~
#### 3.2.1.2 配置協議緩沖區
~~~
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
~~~
在`dependencies`平級添加:
~~~
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
~~~
### 3.2.2 定義架構
在`app/src/main/`目錄下創建`proto`目錄,然后創建一個`xxx.proto`文件:

`testDemo.proto`文件內容:
~~~
syntax = "proto3"; // 聲明proto的版本
// 定義生成的類的包名
option java_package = "com.weizu.jetpackdemo.proto";
// 聲明的是內部類MyProtoBean, 格式: 類型+字段名稱+字段編號
message MyProtoBean {
int32 _id = 1;
string _name = 2;
int32 _age = 3;
bool _isMan = 4;
}
~~~
至于更加詳細的解釋可以查閱:[protobuf 語言指南](https://developers.google.cn/protocol-buffers/docs/proto3)以及[使用 Proto DataStore (google.cn)](https://developer.android.google.cn/codelabs/android-proto-datastore?hl=zh_cn#3)
然后`Build`一下,就可以看到生成的文件:

打開文件可以看到在該文件中生成了配置中對應的`message`類,和對應的`set/get`方法:

至此環境配置完畢,接著開始簡單使用。
### 3.2.3 簡單使用
#### 3.2.3.1 創建序列化器
我們需要實現序列化器,以告知 DataStore 如何讀取和寫入我們在 proto 文件中定義的數據類型。如果磁盤上沒有數據,序列化器還會定義默認返回值。
~~~
object MyBeanSerializer: Serializer<TestDemo.MyProtoBean> {
override val defaultValue: TestDemo.MyProtoBean
get() = TestDemo.MyProtoBean.getDefaultInstance()
override suspend fun readFrom(input: InputStream): TestDemo.MyProtoBean {
try {
return TestDemo.MyProtoBean.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: TestDemo.MyProtoBean, output: OutputStream) {
t.writeTo(output)
}
}
~~~
#### 3.2.3.2 數據存儲和讀取
~~~
/**
* @author 夢否 on 2022/3/28
* @blog https://mengfou.blog.csdn.net/
*/
class MainActivity2 : AppCompatActivity() {
private val textView: TextView by lazy { findViewById(R.id.text) }
private val dataStore: DataStore<TestDemo.MyProtoBean> by lazy {
DataStoreFactory.create(
produceFile = { applicationContext.dataStoreFile("user_prefs.pb") },
serializer = MyBeanSerializer
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private var i = 0
// 按鈕監聽函數
fun buttonOnClick(view: View) {
runBlocking {
launch {
when (view.id) {
R.id.update -> {
storeData(1 + i, 20 + i, true, "張三${i}")
i++
}
R.id.read -> {
val first = readData().first()
Log.e("TAG", "id:${first.id}")
Log.e("TAG", "age:${first.age}")
Log.e("TAG", "isMan:${first.isMan}")
Log.e("TAG", "name:${first.name}")
}
}
}
}
}
// 讀取數據時處理異常
private fun readData(): Flow<TestDemo.MyProtoBean> {
return dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Log.e("TAG", "Error reading sort order preferences.", exception)
emit(TestDemo.MyProtoBean.getDefaultInstance())
} else {
throw exception
}
}
}
// 添加數據
private suspend fun storeData(id: Int, age: Int, isMan: Boolean, name: String) {
dataStore.updateData { preferences ->
preferences.toBuilder()
.setAge(age)
.setId(id)
.setIsMan(isMan)
.setName(name)
.build()
}
}
}
~~~
結果:

每次插入數據都會覆蓋掉以前的數據,也就是在使用對象存儲數據的時候,只能存儲一條數據。