重學Android Jetpack(十)—使用DataStore替代SharedPreferences

語言: CN / TW / HK

theme: geek-black

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第1天,點選檢視活動詳情

簡介

Jetpack DataStore是一種資料儲存解決方案,它使用Protocol Buffers(Kotlin的反射資訊儲存也是這樣格式)協議儲存鍵值對或型別化物件。DataStore 使用Kotlin協程和Flow以非同步、一致的事務方式儲存資料。這裡要注意的是,DataStore比較適合簡單的小型資料儲存,不支援部分更新或參照完整性,如果我們需要支援大型或複雜的資料、部分更新或參照完整性,請考慮使用Room,而不是DataStore

SharedPreferences和DataStore的比較

很顯然DataStore的出現就是要代替SharedPreferences的使用,那麼DataStore對於SharedPreferences的優勢在哪裡呢?我們先看SharedPreferences存在的一些問題:

SharedPreferences的不足
  • SharedPreferences在儲存稍微大點的檔案會存在阻塞UI執行緒而導致ANR,而且不可以在跨程序使用。(說起這個,小弟剛來目前的公司時發現公司的專案有在跨程序使用SharedPreferences,專案看似執行正常,後面在使用者使用反饋分析中得知一些偶現的問題正是因為跨程序使用SharedPreferences導致的,只能說真是坑啊…)。

  • 在多執行緒場的情況使用的效率會比較低,這是因為在get操作的時會鎖定SharedPreferencesImpl裡面的物件,互斥其他操作,而當 put、commit()apply()操作的時候都會鎖住Editor的物件。

  • 每次載入檔案的時候都會把資料載入到記憶體中,而資料會一直存在記憶體中,會造成記憶體浪費。如果檔案稍大,讀取也會比較慢,還會引發程式的頻繁gc,引起卡頓的問題。

  • 通過SharedPreferences進行資料儲存時無法獲取資料儲存成功的回撥。

DataStore的優勢
  • DataStore的設計旨在代替SharedPreferences,官方也建議把資料遷移到 DataStore

  • DataStore是基於Kotlin協程開發的,並支援Flow,所以它在主執行緒操作是安全的。

  • DataStore提供了兩種實現:

    • Preferences DataStore 使用鍵儲存和訪問資料。此實現不需要預定義的架構,也不確保型別安全。
    • Proto DataStore 將資料作為自定義資料型別的例項進行儲存。此實現要求我們使用Protocol Buffers格式協議來定義架構,但可以確保型別安全。
  • DataStore以事務方式處理更新資料,事務有四大特性:原子性、一致性、 隔離性、永續性。

所以,使用DataStore來儲存資料會是一種更優的方案。

Preferences DataStore 與 Proto DataStore 區別
  • Preferences DataStore是根據鍵訪問xml檔案儲存的資料,無需事先定義架構,解決了SharedPreferences的不足;
  • Proto DataStore使用protocol buffers協議來定義架構,可持久保留強型別資料,並保證型別安全,與xml儲存相比protocol buffers協議儲存速度更快、規格更小、使用更簡單,並且更清楚明瞭,但需要學習新的序列化機制。

DataStore的使用

Preferences DataStore使用

依賴:

implementation "androidx.datastore:datastore-preferences:1.0.0"

定義Preferences DataStore檔名和儲存資料的key:

``` const val PDS = "pds"

val KEY_USER_NAME = stringPreferencesKey("userName") ```

建立Preferences DataStore

我們宣告一個DataStoreExt.kt的檔案來建立,類似頂層函式,方便全域性呼叫:

val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = PreferencesConstant.PDS)

在ViewModel中宣告儲存和獲取資料的方法:

``` class MainViewModel : ViewModel() {

val preLiveData = MutableLiveData<String>()

//儲存資料
fun putValue(dataStore: DataStore<Preferences>, content: String, key: Preferences.Key<String>) {
    viewModelScope.launch(Dispatchers.IO) {
        dataStore.edit { settings ->
            settings[key] = content
        }
    }
}

//獲取資料
fun getValue(dataStore: DataStore<Preferences>,key: Preferences.Key<String>) {
    viewModelScope.launch(Dispatchers.IO) {
        dataStore.edit { settings ->
            val text = settings[key]
            preLiveData.postValue(text)
        }
    }
}

  /**
 * 清楚所有鍵
 */
fun clearPreferences(dataStore: DataStore<Preferences>){
    viewModelScope.launch {
        dataStore.edit {
            it.clear()
        }
    }
}

}

```

Activity中通過ViewModel來使用:

``` class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel

private val btnPut: Button by lazy {
    findViewById(R.id.btn_put)
}

private val btnGet: Button by lazy {
    findViewById(R.id.btn_get)
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel = ViewModelProvider(this)[MainViewModel::class.java]

    btnPut.setOnClickListener {
        viewModel.putValue(dataStore,"張三",PreferencesConstant.KEY_USER_NAME)

        Toast.makeText(this,"已儲存資料",Toast.LENGTH_SHORT).show()
    }

    btnGet.setOnClickListener {
        viewModel.getValue(dataStore,PreferencesConstant.KEY_USER_NAME)
    }

    viewModel.preLiveData.observe(this){

        Toast.makeText(this,it,Toast.LENGTH_SHORT).show()
    }
}

} ```

效果:

QQ圖片20220623221407.gif

使用Proto DataStore儲存資料

依賴

``` plugins { id "com.google.protobuf" version "0.8.12" }

dependencies { implementation "androidx.datastore:datastore:1.0.0" //protobuf implementation "com.google.protobuf:protobuf-javalite:3.14.0" }

protobuf { protoc { // 這裡設定protoc的版本要跟protobuf-javalite的版本一致 artifact = 'com.google.protobuf:protoc:3.14.0' } generateProtoTasks { all().each { task -> task.builtins { java { option "lite" } } } } }

```

預定義架構建立

Proto DataStore的實現是使用DataStore和協議緩衝區(Protocol Buffers)將型別化的物件保留在磁碟上。

要使用Proto DataStore我們首先要利用協議緩衝區定義架構,通過使用協議,DataStore可以知道儲存的型別,並且無需使用鍵便能提供型別。

新建一個proto的資料夾,路徑是:

1656004036771.png

定義架構的程式碼protobuf語言,這裡是照著官方做的,這個語言暫時也不是很熟悉就不展開細講了,理解基本的也不影響我們使用,我們定義一個user_prefs.proto檔案,裡面程式碼如下:

``` syntax = "proto3";

option java_package = "com.qisan.datastoredemo"; option java_multiple_files = true;

message UserInfoPreference { // 下面定義分別是 欄位 型別 名稱 編號 也就是我們定義實體中的元素 string name = 1; int32 age = 2; } ```

接著建立一個與預定義結構中資料結構相同的資料實體:

data class UserInfo( val name: String = "張三", val age: Int = 23 )

建立Serializer

``` object UserInfoSerializer : Serializer {

override suspend fun readFrom(input: InputStream): UserInfoPreference {
    try {
        return parseFrom(input)
    } catch (exception: Exception) {
        throw CorruptionException("Cannot read proto.", exception)
    }
}

override suspend fun writeTo(t: UserInfoPreference, output: OutputStream) {
    t.writeTo(output)
}

override val defaultValue: UserInfoPreference
    get() = UserInfoPreference.getDefaultInstance()

} ```

建立Proto DataStore

我們還是把它定義在Kotlin檔案頂層呼叫:

``` const val USER_PB = "userInfo.pb"

val Context.userInfoStore: DataStore by dataStore( fileName = PreferencesConstant.USER_PB, serializer = UserInfoSerializer ) ```

ViewModel新增儲存資料和獲取資料的方法

``` class MainViewModel : ViewModel() {

val userInfoLiveData = MutableLiveData<UserInfoPreference>()

/**
 * Proto dataSotre put
 */
fun putProtoValue(dataStore: DataStore<UserInfoPreference>,userInfo: UserInfo) {
    viewModelScope.launch(Dispatchers.IO) {
        dataStore.updateData { user ->
            user.toBuilder().setName(userInfo.name).setAge(userInfo.age).build()
        }
    }
}

/**
 *  Proto dataSotre get
 */
suspend fun getProtoValue(dataStore: DataStore<UserInfoPreference>) {
    dataStore.data.collect{
        userInfoLiveData.postValue(it)
    }
}

} ```

Activity中呼叫:

``` btnPut.setOnClickListener { viewModel.putProtoValue(userInfoStore,UserInfo("李四",26)) Toast.makeText(this,"已儲存資料",Toast.LENGTH_SHORT).show() }

    btnGet.setOnClickListener {
        lifecycleScope.launch {
            viewModel.getProtoValue(userInfoStore)
        }
    }

    viewModel.userInfoLiveData.observe(this){
        Toast.makeText(this,"${it.name} 已經 ${it.age} 歲了",Toast.LENGTH_SHORT).show()
    }

```

效果:

QQ圖片20220624012817.gif

總結

DataStore的基本使用已經介紹完了,Preferences DataStore其實很好理解,跟之前SharePreferences的用法比較相像,Proto DataStore相對來說難一點,又涉及到了protobuf的使用,我對這個語言也一點不理解,但是按照官方的演示架構也是可以使用,複雜的情況下需要要了解一下protobuf。一般開發的情況我們只是用來儲存一些諸如token之類的資訊,Preferences DataStore是完全可以滿足我們的需求。當然,我們也可以去了解一下使用MMKVK框架,小弟覺得這也是一個替代SharePreferences比較好的框架。