天天看點

Android Jetpack 之 DataStore 初探

前言

DataStore是google官方提供的,用于替換SharedPreferences來對簡單資料進行存儲的解決方案。對于SharedPreferences存在的缺陷

進行修補,并且可以和Kotlin協程與Flow結合。

DataStore與SharedPreferences 功能對比
Android Jetpack 之 DataStore 初探
DataStore 功能

DataStore提供對基本類型和對象類型進行分開存儲,分别使用 PreferencesDataStore,ProtoDataStore.通過名字很容區分 PreferencesDataStore用來存儲基本類型資料,String、int、long、double、float、boolean、set< String >。

Proto DataStore 使用協定緩沖區來定義架構。使用協定緩沖區可持久保留強類型資料。與 XML 和其他類似的資料格式相比,協定緩沖區速度更快、規格更小、使用更簡單,并且更清楚明了。雖然使用 Proto DataStore 需要學習新的序列化機制,但 Proto DataStore 有着強大的類型優勢,值得學習。

并且提供了友善的遷移接口,對于基礎類型存儲可以通過配置自動實作遷移。

PreferencesDataStore 配置依賴

dependencies {
 //preferencesDatastore功能依賴
 implementation "androidx.datastore:datastore-preferences:1.0.0"
 //protoDatastore功能依賴
 implementation  "androidx.datastore:datastore-core:1.0.0"
 implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
 implementation  "androidx.datastore:datastore:1.0.0"
 
 }      
PreferencesDataStore 初始化
//user_preferences 生成的檔案名稱 user_preferences.preferences_pb
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(USER_PREFERENCES_NAME)      

PreferencesDataStore 讀取寫入:需要放在協程中進行讀寫

private suspend fun spStoreSave() {
        dataStore.edit { preferences ->
            preferences[stringPreferencesKey("one1")] = "中華人民共和國"
            preferences[intPreferencesKey("one2")] = 1
            preferences[doublePreferencesKey("one3")] = 1.00
            preferences[floatPreferencesKey("one4")] = 1f
            preferences[longPreferencesKey("one5")] = 1L
            preferences[booleanPreferencesKey("one6")] = true
            preferences[stringSetPreferencesKey("one7")] = setOf("1", "2", "3", "4", "5", "6")
        }
    }      

讀取

private suspend fun getSpStore() {
        val flow = 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 preferences.", exception)
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preferences ->
            println(preferences[stringPreferencesKey("one1")])
            println(preferences[intPreferencesKey("one2")])
            println(preferences[doublePreferencesKey("one3")])
            println(preferences[floatPreferencesKey("one4")])
            println(preferences[longPreferencesKey("one5")])
            println(preferences[booleanPreferencesKey("one6")])
            preferences[stringSetPreferencesKey("one7")]

        }
        flow.collect()

    }      

生成的檔案内部結構 user_preferences.preferences_pb

Android Jetpack 之 DataStore 初探
PreferencesDataStore 與 SharedPreferences對比:大小、内部結構

SharedPreferences寫入相同的資料後進行對比

private fun saveUserInfo() {
        val userInfo = getSharedPreferences(SETTING, MODE_PRIVATE)
        val editor = userInfo.edit() //擷取Editor
        editor.apply {
            putString("one1", "中華人民共和國")
            putInt("one2", 1)
            putFloat("one3",1.00f)
            putFloat("one4",1f)
            putLong("one5",1L)
            putBoolean("one6",true)
            putStringSet("one7", setOf("1", "2", "3", "4", "5", "6"))
        }
        editor.commit() //送出修改
    }      

檔案内部結構對比。左邊為SharedPreferences存儲,右邊為PreferencesDataStore 存儲。

Android Jetpack 之 DataStore 初探
Android Jetpack 之 DataStore 初探

相同寫入相同内容,SharedPreferences比PreferencesDataStore存儲大小多了3倍。

SharedPreferences遷移PreferencesDataStore

需要指定要遷入的 PreferencesDataStore 檔案名稱和要遷出的檔案SharedPreferences檔案名稱,如下:

//SharedPreferences 舊存儲結構SharedPreferences
private const val  SETTING = "setting"
//要遷入的新存儲結構 PreferencesDataStore 
private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
        // Since we're migrating from SharedPreferences, add a migration based on the
        // SharedPreferences name
        listOf(SharedPreferencesMigration(context, SETTING))
    }
)      

重新編譯運作項目後檢查 裝置路徑:data/data/packageName/files/datastore/ xx.preferences_pb檔案,如果檔案中有SharedPreferences内容,表示自動遷移完成了。

Proto DataStore

Proto DataStore 是用來存取可序列化對象,SharedPreferences 不能直接對對象進行存取,開發中 一般做法會将對象轉換成jsonString進行存儲,需要取出時再由取出來的jsonStrinng轉換成需要的Bean檔案。如果使用Proto DataStore存取那麼可以直接取出定義的Bean對象。

1.添加protoBuffer 模闆代碼生成插件 和 Proto DataStore 依賴項

在項目app子產品 build.gradle 檔案中添加自動生成模闆代碼的插件和必要的依賴項

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id "com.google.protobuf" version "0.8.17"
}      

//protobuf閉包 與 android閉包同一級

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.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'
                }
            }
        }
    }
}

dependencies {

    //protocol cache
    implementation  "androidx.datastore:datastore-core:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"

    implementation("androidx.datastore:datastore:1.0.0")
}      
2.定義protobuf 對象并實作序列化(需要額外學習成本)

需要在**/app/src/main/proto**在建立一個字尾為.proto的 protoBuffer檔案,比如這裡建立一個名為

person.proto的檔案。 之後需要手動進行編譯,可以通過檢查app/build/generrated/source/proto/debug/java/包名/下

syntax = "proto3";

// 預設生成路徑 ${project.buildDir}/generated/source/proto
option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message Person {
  //學号
  int32 id = 1;
  //姓名
  string name = 2;
  //年齡
  int32 age = 3;
  //班級
  string class = 4;
}      

建立一個序列化器,都是一樣的寫法,照着模闆寫就行

//建立序列化器

object PersonPreferencesSerializer : Serializer<Person> {

    override val defaultValue: Person = Person.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): Person {
        try {
            return Person.parseFrom(input)
        } catch (e: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", e)
        }
    }

    override suspend fun writeTo(t: Person, output: OutputStream) {
        t.writeTo(output)
    }
}      
3.存取 protobuf 對象,并輸出
//存
 lifecycleScope.launch {
                persion.updateData { preferenceChanges ->
                    preferenceChanges.toBuilder()
                        .setId(10000)
                        .setAge(1)
                        .setName("小明")
                        .setClass_("一年級")
                        .build()
                }
            }
            
//取

       lifecycleScope.launch {
     persion.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(Person.getDefaultInstance())
         } else {
             throw exception
         }
     }.map {
         with(it) {
             println("class =$class_ name =$name age =$age id =$id")
         }
     }.collect()
 }      
proto DataStore 遷移

SharedPreferences 檔案遷移到ProtoDataStore,需要手動取出來SharedPreferences記憶體儲值,通過事務Build設定給Proto對象。需要注意的一點是每一個定義的proto 對象為一個單獨的檔案。如果SharedPreferences 記憶體儲值需要轉存入Proto對象中。需要在如下代碼Listof(SharedPreferencesMigration(),SharedPreferencesMigration(), …)定義多個接收SharedPreferences值的對象。

private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(fileName = 對象.pb檔案名稱,
    serializer = UserPreferencesSerializer,
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(
            context, Sp檔案名稱
        ) { sharedPreferences: SharedPreferencesView, currentData: UserPreferences ->
            if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
                currentData.toBuilder().setSortOrder(
                    SortOrder.valueOf(
                        sharedPreferences.getString(
                            SORT_ORDER_KEY, SortOrder.NONE.name
                        )!!
                    )
                ).build()
            } else {
                currentData
            }
        })
    })      
總結: