來實現一個DataStore的封裝吧

之前Content Provider,Room,DataStore一起弄,對於蒟蒻我來說步子邁得太大了,bug滿天飛(DataStore一直給我報錯說同時打開了多個DataStore,卻又找不到問題所在),遂不得不暫且拋下DataStore換回SharedPreference,後來才發現是被Hook應用在啟動的時候,LSPosed檢測了新Module導致同時Hook到了模組本身,然而模組里又沒忽略自己,導致兩個Content Resolver同時請求了兩次數據。

既然bug已經解決了,那大可以把SharedPreference給扔了,由於Project目前過於混亂,不僅所有業務邏輯居然都在ViewModel和Provider里,而且SharedPreference的調用也分別在MainViewModel和MainProvider里寫了兩次,極為Chulu,為了以後自己和項目不爆炸,不得不學習一下工具類的封裝,免得重複邏輯飛得到處都是。

然後就開始寫DataStoreUtil,由於會被重複使用,所以必須使其遵循單例模式,因為本蒟蒻對單例模式這些東西一無所知,連object關鍵字都不知道是幹嘛的,所以瞎學一氣後,使用了最為簡單的懶漢模式,直接用object定義一個單例,反正程式在啟動時本來就要初始化DataStore的,懶漢模式的缺點在此就不構成影響,雙重校驗鎖這種高級東西以後有需要再用吧

由於Project其他部分的邏輯還不支援非同步,所以我這裡之暴露的同步操作的方法,以後支援非同步了後,只要把private刪掉就可以了。這回順便還學習了個泛型,這樣就可以用一個方法來處理不同的類型,不用像以前那樣,定義一堆類似getBooleanPreferencegetStringPreference的方法了。

val Context.dataStore by preferencesDataStore(name = "settings")

object DataStoreUtil {

    private lateinit var dataStore: DataStore<Preferences>
    fun initialize(context: Context) { dataStore = context.dataStore }

    fun <T> getPreference(key: String, defaultValue: T): T {
        return runBlocking { asyncGetPreference(key, defaultValue).first() }
    }

    fun <T> setPreference(key: String, value: T) {
        runBlocking { asyncSetPreference(key, value) }
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> asyncGetPreference(key: String, defaultValue: T): Flow<T> {
        return when (defaultValue) {
            is String -> dataStore.data.map { it[stringPreferencesKey(key)] ?: defaultValue }
            is Boolean -> dataStore.data.map { it[booleanPreferencesKey(key)] ?: defaultValue }
            else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
        } as Flow<T>
    }

    private suspend fun <T> asyncSetPreference(key: String, value: T) {
        when (value) {
            is String -> dataStore.edit { it[stringPreferencesKey(key)] = value }
            is Boolean -> dataStore.edit { it[booleanPreferencesKey(key)] = value }
            else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
        }
    }
}

使用例:

var dataStore: DataStoreUtil = DataStoreUtil.apply { initialize(context) }
val appTheme = dataStore.getPreference("theme", "light")
dataStore.setPreference("theme", "dark")

得益於Kotlin提供的內置拓展函數,原本需要兩行的的程式碼被精簡到了一行,美麗了不少。

除了搓一個單例,也可以用Kotlin的委託功能,像使用普通變數一樣進行DataStore的存取操作。

class dataStoreVariable <T> (private val context: Context, private val key: String, private val defaultValue: T) : ReadWriteProperty<Any?, T> {

    private val dataStore by lazy { context.dataStore }

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return when (defaultValue) {
            is String -> runBlocking { dataStore.data.map { it[stringPreferencesKey(key)] ?: defaultValue } }
            is Boolean -> runBlocking { dataStore.data.map { it[booleanPreferencesKey(key)] ?: defaultValue } }
            else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
        } as T
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        when (value) {
            is String -> runBlocking { dataStore.edit { it[stringPreferencesKey(key)] = value } }
            is Boolean -> runBlocking { dataStore.edit { it[booleanPreferencesKey(key)] = value } }
            else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
        }
    }
}

使用例:

var appTheme: String by dataStoreVariable(activity, "theme", "light")
appTheme = "dark"

看起來也非常不錯,不過這裡只寫了同步的存取操作,如果需要非同步只需要稍作修改即可。

當然,使用Kotlin的擴展功能也是可以實現的,就像給Context擴展dataStore一樣,可以給Context擴展對DataStore進行各種操作的方法:

@Suppress("UNCHECKED_CAST")
fun <T> Context.getDataStoreValue(key: String, defaultValue: T): T {
    val dataStore = dataStore
    return when (defaultValue) {
        is String -> runBlocking { dataStore.data.map { it[stringPreferencesKey(key)] ?: defaultValue }.first() }
        is Boolean -> runBlocking { dataStore.data.map { it[booleanPreferencesKey(key)] ?: defaultValue }.first() }
        else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
    } as T
}

fun <T> Context.setDataStoreValue(key: String, value: T) {
    val dataStore = dataStore
    when (value) {
        is String -> runBlocking { dataStore.edit { it[stringPreferencesKey(key)] = value } }
        is Boolean -> runBlocking { dataStore.edit { it[booleanPreferencesKey(key)] = value } }
        else -> throw IllegalArgumentException("Wrong value provided with invalid value type")
    }
}

然後就只需要拿著手上有的context,就可以獲取DataStore里的值了:

var appTheme = context.getDataStoreValue("theme", "light")
context.setDataStoreValue("theme", "dark")

雖然說,工具類是一種違反面向對象思想的東西,應該多利用kotlin的特性(委託、擴展),但自己還是太菜了,所以這回在Project里依然選擇了單例,希望以後能儘可能把後面這兩種實現給用起來,還有太多東西需要學了(逃)。

Tags: