天天看點

Android Jetpack元件 DataStore的使用和簡單封裝

Jetpack元件 DataStore的使用和簡單封裝

  • ​​前言​​
  • ​​正文​​
  • ​​一、添加依賴​​
  • ​​二、資料存取​​
  • ​​三、資料檢視和清除​​
  • ​​四、封裝​​
  • ​​五、對象存取​​
  • ​​1. 插件安裝​​
  • ​​① 添加協定緩沖區插件​​
  • ​​② 添加協定緩沖區和 Proto DataStore 依賴項​​
  • ​​③ 配置協定緩沖區​​
  • ​​2. 建立proto檔案​​
  • ​​3. 配置proto檔案​​
  • ​​4. 建立序列化器​​
  • ​​5. 對象寫入和取出​​
  • ​​六、源碼​​

前言

  也許你是第一次聽說這個DataStore,也許你有所耳聞,但從未使用過,不過都沒有關系,随着這篇文章去熟悉DataStore。

正文

  DataStore是Jetpack中的一個元件,用于做資料持久化,DataStore以異步、一緻的事務方式存儲資料,克服了SharedPreferences的一些缺點,DataStore基于Kotlin協程和Flow實作,就是用來取代SharedPreferences的。我們廢話不多說,開始吧。按照慣例,我們建立一個項目去做示範,不過稍微有一些不同,這次我們建立的項目時Kotlin語言的,請注意。

Android Jetpack元件 DataStore的使用和簡單封裝

建立好項目,待項目配置完成之後,我們添加依賴。

一、添加依賴

在app子產品下的build.gradle中的dependencies{}閉包中添加如下依賴:

//DataStore
    implementation 'androidx.datastore:datastore-preferences:1.0.0'
    implementation 'androidx.datastore:datastore-preferences-core:1.0.0'      

同時添加開啟ViewBinding和DataBinding,如下圖所示:

Android Jetpack元件 DataStore的使用和簡單封裝

然後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去進行資料存取,還有一些其他的方法可供你使用。

Android Jetpack元件 DataStore的使用和簡單封裝

基本上滿足你的要求,SP的功能它肯定都會有的,這裡這些方法可以快速建構一個符合類型的Key。

下面我們寫一個方法進行存資料,代碼如下:

private suspend fun put() = dataStore.edit { it[key] = "疫情" }      

這裡用到了Kotlin的協程,如果你對這個不太了解,那麼也沒有關系,你先知道這麼用,然後再去了解協程。這個方法這樣不太清晰,換種方式:

Android Jetpack元件 DataStore的使用和簡單封裝

通過dataStore.edit函數,裡面的it就是MutablePreferences,然後我們通過key去設定它的值,這裡是設定疫情兩個字。而這個suspend是協程中的關鍵字,你現在可以将這個put()當成是在子線程中執行的,那麼執行結束之後需要怎麼做呢?需要切換到主線程。這是在調用的地方進行切換,比如我們在點選存資料按鈕的時候調用,如下圖所示:

Android Jetpack元件 DataStore的使用和簡單封裝

就是這樣的。

下面我們再寫一個取資料的方法。

private fun get() = runBlocking {
        return@runBlocking dataStore.data.map { it[key] ?: "新冠" }.first()
    }      

你會發現和存資料又有不同,這裡的first()就是取值,這個方法換個方式來看就清晰一些。

Android Jetpack元件 DataStore的使用和簡單封裝

然後我們在取資料按鈕的點選事件中調用。

Android Jetpack元件 DataStore的使用和簡單封裝

下面我們運作一下:

Android Jetpack元件 DataStore的使用和簡單封裝

  第一次我先取資料,顯示的是預設值,然後我存資料再取資料。效果就是這樣,但你會覺得使用起來很麻煩,不如SP好用,這個我們後面再去封裝,先了解一些它的功能特性。

三、資料檢視和清除

  在進行定義dataStore時,會在手機中生成一個pb檔案,這裡我們用虛拟機來看,

Android Jetpack元件 DataStore的使用和簡單封裝

然後通過你的程式包名去找

Android Jetpack元件 DataStore的使用和簡單封裝

這裡的檔案就是存放你的緩存資訊的檔案。這裡我用txt打開看一下

Android Jetpack元件 DataStore的使用和簡單封裝

可以看到鍵和值,也許是浏覽檔案不對,下面我們清理一下這個資料。在布局中增加一個按鈕

Android Jetpack元件 DataStore的使用和簡單封裝

在代碼中

Android Jetpack元件 DataStore的使用和簡單封裝

通過clear方法調用進行資料的清除,清除後我們再看看這個pb檔案

Android Jetpack元件 DataStore的使用和簡單封裝

這個檔案就什麼都沒有了,清除的幹幹淨淨。

四、封裝

  這個DataStore是肯定需要封裝之後再使用的,直接使用太麻煩了,我們需要封裝的像SP那樣好用,資料類型就參考這個方法中的資料類型。

Android Jetpack元件 DataStore的使用和簡單封裝

在寫封裝代碼之前呢,我們先建立一個App類,裡面的代碼如下:

class App : Application() {

    companion object {
        lateinit var instance : App
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}      

然後我們在AndroidManifest中設定

Android Jetpack元件 DataStore的使用和簡單封裝

下面我們建立一個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中使用一下:

Android Jetpack元件 DataStore的使用和簡單封裝

這裡我們存資料、取資料、清空資料都用到了,下面運作一下:

Android Jetpack元件 DataStore的使用和簡單封裝

對于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      
Android Jetpack元件 DataStore的使用和簡單封裝

再打開app下的build.gradle,添加如下代碼:

'com.google.protobuf'      
Android Jetpack元件 DataStore的使用和簡單封裝

② 添加協定緩沖區和 Proto DataStore 依賴項

在app的dependencies{}閉包中添加如下代碼:

//Proto DataStore
    implementation  'androidx.datastore:datastore-core:1.0.0'
    implementation  'com.google.protobuf:protobuf-javalite:3.10.0'      
Android Jetpack元件 DataStore的使用和簡單封裝

③ 配置協定緩沖區

在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'
                }
            }
        }
    }
}      

注意它添加的位置:

Android Jetpack元件 DataStore的使用和簡單封裝

點選Sync Now。

2. 建立proto檔案

将項目切換到Project,然後在main下面建立一個proto檔案夾。

Android Jetpack元件 DataStore的使用和簡單封裝

在此檔案夾下建立study.proto檔案,然後AS會發現打開這個格式需要安裝一個插件。

Android Jetpack元件 DataStore的使用和簡單封裝

點選Install plugins進行安裝。

Android Jetpack元件 DataStore的使用和簡單封裝

安裝成功之後,重新開機AS插件生效。

Android Jetpack元件 DataStore的使用和簡單封裝

注意看這個檔案的圖示變了,這說明你的插件安裝成功并且配置成功了。

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中增加兩個按鈕,如下所示:

Android Jetpack元件 DataStore的使用和簡單封裝

然後回到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}"
            }
        }      

繼續閱讀