再抱一抱DataStore

简介: DataStore 中使用到了 Flow 、协程,在使用的时候更适合现在的写法,但不能只因为他的写法简单就使用啊,目前即使DataStore 已经发布了 beta 版本,但是性能还是堪忧

一片爆红

在之前我发过一篇 DataStore 的文章:用力抱一下 Jetpack DataStore,当时使用的版本是1.0.0-alpha05

20210524100401171.png

大家也都知道 Google 的性格,在 alpha 版本的库的 API 随时可能修改,事实证明我的猜测是正确的,在更新了 DataStore 的版本之后果然一片爆红。

* init Context 
*@ param context Context 
 fun init ( context : Context ){
 dataStore = context . createDataStore ( preferenceName )
}
 fun readIntFlow ( key : String , default : Int =0): Flow < Int >=
 dataStore . data 
. Catch {
 this : FlowCollector < Preferences >
 if ( it is IoException ){
 it . printStackTrace ()
 emit ( emptyPreferences ())} else {
 throw it 
}. map { it : Preferences 
 it [ preferencesKey ( key )]?: default 
}
 fun readIntData ( key : String , default : Int =0): Int {
 var yalue =0
 runBlocking { this : CoroutineScope 
 dataStore . data . first { it : Preferences 
 value = it [ preferencesKey ( key )]?: default 
 true 
 first 
}
}
 return value 

但不要担心,发布了 beta 版本就证明 API 已经基本完善了,也就是说之后 API 就不会有大的改动了,剩下的就是性能方面的优化了。

开始使用

之前为了使用 DataStore 我还专门写了一个工具类,但现在看来是多余的了,来看看如何使用 beta 版本的 DataStore 吧。

添加依赖

第一步肯定要来添加下依赖。

// DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-beta01"

初始化 DataStore

在之前 alpha 版本中,初始化方法如下:

context.createDataStore(preferenceName)

通过 Context 的扩展方法 createDataStore 来创建一个 DataStore ,但现在不可以了,这个方法已经被删除,那么现在该如何创建 DataStore 呢?现在应该使用由 preferencesDataStore 创建的属性委托来创建 Datastore<Preferences> 实例。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "PlayAndroidDataStore")

在项目中只在顶层调用一次 preferencesDataStore 方法,便可在应用的所有其余部分通过此属性访问该实例,这样可以更轻松地将 DataStore 保留为单例。

下面咱们来看看 preferencesDataStore 的源码吧。

public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}

可以看到 preferencesDataStore 有四个参数,其中只有名字是必选参数,剩下的参数都有对应的默认值,下面就来看看这几个参数的含义吧。

  • name: DataStore 的名字,不多说;
  • corruptionHandler:如果DataStore在尝试读取数据时遇到CorruptionException,则将调用destroyHandler。无法对数据进行反序列化时,序列化程序会引发CorruptionException;
  • produceMigrations:产生迁移。 ApplicationContext作为参数传递给这些回调。在对数据进行任何访问之前,都要运行DataMigrations。不管是否成功,每个生产者和迁移都可以运行一次以上(可能是因为另一个迁移失败或对磁盘的写入失败)。
  • scope:作用域

上面介绍了 preferencesDataStore 方法中的参数,大家可以根据需求自行填写使用。

使用DataStore

创建 DataStore

使用的时候首先需要调用下咱们刚定义的 Context 的扩展属性:dataStore。

private lateinit var dataStore: DataStore<Preferences>
/**
 * init Context
 * @param context Context
 */
fun init(context: Context) {
    dataStore = context.dataStore
}

之后就可以通过dataStore进行操作了。

保存数据

先来看看如何进行数据的保存。

suspend fun saveBooleanData(key: String, value: Boolean) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[booleanPreferencesKey(key)] = value
    }
}
suspend fun saveIntData(key: String, value: Int) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[intPreferencesKey(key)] = value
    }
}
suspend fun saveStringData(key: String, value: String) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[stringPreferencesKey(key)] = value
    }
}
suspend fun saveFloatData(key: String, value: Float) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[floatPreferencesKey(key)] = value
    }
}
suspend fun saveLongData(key: String, value: Long) {
    dataStore.edit { mutablePreferences ->
        mutablePreferences[longPreferencesKey(key)] = value
    }
}

大家在使用的时候可以根据不同类型来分别进行保存,当然了,如果你不想分的这么细,也可以写一个方法来进行统一调用。

suspend fun <U> putData(key: String, value: U) {
    when (value) {
        is Long -> saveLongData(key, value)
        is String -> saveStringData(key, value)
        is Int -> saveIntData(key, value)
        is Boolean -> saveBooleanData(key, value)
        is Float -> saveFloatData(key, value)
        else -> throw IllegalArgumentException("This type can be saved into DataStore")
    }
}

读取数据

保存完数据就该进行读取了,来看看如何读取数据吧。

fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
    dataStore.data
        .catch {
            //当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
            //但是如果是其他的异常,最好将它抛出去,不要隐藏问题
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[booleanPreferencesKey(key)] ?: default
        }
fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[intPreferencesKey(key)] ?: default
        }
fun readStringFlow(key: String, default: String = ""): Flow<String> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[stringPreferencesKey(key)] ?: default
        }
fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[floatPreferencesKey(key)] ?: default
        }
fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
    dataStore.data
        .catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[longPreferencesKey(key)] ?: default
        }

同样的,根据类型不同选择不同的读取方法进行使用即可,当然了,也可以像上面保存数据一样再写一个方法进行统一调用也可以。

fun <U> getData(key: String, default: U): Flow<U> {
    val data = when (default) {
        is Long -> readLongFlow(key, default)
        is String -> readStringFlow(key, default)
        is Int -> readIntFlow(key, default)
        is Boolean -> readBooleanFlow(key, default)
        is Float -> readFloatFlow(key, default)
        else -> throw IllegalArgumentException("This type can be saved into DataStore")
    }
    return data as Flow<U>
}

进阶使用

上面已经描述了如何简单使用 DataStore ,但只能使用一些基础类型,如果咱们想要使用数据类呢?当然可以使用 DataStore 所支持的 Proto,但这里咱们不说这个,应为在 Kotlin 中也可以直接通过数据类序列化的方式来使用 DataStore ,来看看如何使用吧。

定义数据类

首先咱们来定义一个数据类吧。

data class ZhuPreferences(
    val name: String,
    val age: Int
)

数据类很简单,只有两个参数,名字和年龄。

实现 DataStore 序列化器

下一步需要做的是来实现 DataStore 的序列化器。

@Serializable
data class ZhuPreferences(
    val name: String = "jiang",
    val age: Int = 20
)
object ZhuPreferencesSerializer : Serializer<ZhuPreferences> {
    override val defaultValue = ZhuPreferences()
    override suspend fun readFrom(input: InputStream): ZhuPreferences {
        try {
            return Json.decodeFromString(
                ZhuPreferences.serializer(), input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read UserPrefs", serialization)
        }
    }
    override suspend fun writeTo(t: ZhuPreferences, output: OutputStream) {
        output.write(Json.encodeToString(ZhuPreferences.serializer(), t).encodeToByteArray())
    }
}

由于 Parcelables 与 DataStore 一起使用并不安全,因为不同 Android 版本之间的数据格式可能会有所变化,所以这里使用 Serializable 的方式进行序列化。

使用 DataStore 序列化器

创建 DataStore

数据类都准备好了,下面来创建 DataStore ,这里不能使用上面提到的 preferencesDataStore 方法来进行创建了,而是需要通过 dataStore 方法。

val Context.dataStores by dataStore("test", serializer = ZhuPreferencesSerializer)

写入数据

来看看如何写入数据类的数据吧。

private suspend fun setDataStore(zhuPreferences: ZhuPreferences) {
    dataStores.updateData {
        it.copy(name = zhuPreferences.name, age = zhuPreferences.age)
    }
}

从上面代码中可以看到通过 dataStores 中的 updateData 方法来进行修改数据,然后再使用生成的 .copy() 函数更新数据。

读取数据

写入完成之后来看看如何进行读取数据吧。

private suspend fun getDataStore() {
    val name = dataStores.data.first().name
    val age = dataStores.data.first().age
}

由于在创建 dataStores 的时候已经传入了序列化器,所以读取数据很简单,直接可以调取数据类中的参数进行使用。

总结

DataStore 中使用到了 Flow 、协程,在使用的时候更适合现在的写法,但不能只因为他的写法简单就使用啊,目前即使DataStore 已经发布了 beta 版本,但是性能还是堪忧,但我相信 DataStore 在正式版本发布的时候性能应该会追上甚至超越 MMKV 和 SP 。



目录
相关文章
|
3月前
|
移动开发 vr&ar 数据安全/隐私保护
no-strings-attached-入土为安的第十一天
no-strings-attached-入土为安的第十一天
29 0
|
Java 索引
24Solr项目案例
24Solr项目案例
25 0
|
安全 Shell PHP
Ichunqiu云境 - Delegation Writeup
1. Target external IP 39.98.34.149 2. Nmap results
|
Ubuntu Unix Linux
听说,有个同事因为关闭服务器被打进 ICU ……
对于 Linux 电脑,正常情况下你是如何关机的?
120 0
听说,有个同事因为关闭服务器被打进 ICU ……
|
存储 监控 Oracle
外行假装内行,我也来谈谈SAP BAPI和BADI
外行假装内行,我也来谈谈SAP BAPI和BADI
|
测试技术 API 网络架构
堪称神器的Wisdom RESTClient 你忍心拒绝吗?
Wisdom RESTClient是一款支持自动化测试RESTful API并且可以基于测试的历史数据自动生成RESTful API文档的REST Client神器。
4907 0
|
SQL Web App开发
艾伟:从别人那拷下来的几点Session使用的经验(转载)
问:当页面中是否了frameset,发现在每个frame中显示页面的SessionID在第一次请求时都不相同,为什么?答:原因是你的frameset是放在一个htm页面上而不是ASPX页面。在一般情况下,如果frameset是aspx页面,当你请求页面时,它首先将请求发送到Web服务器,此时已经获得了SessionID,接着浏览器会分别请求Frame中的其他页面,这样所有页面的SessionID就是一样的,就是FrameSet页面的SessionID。
878 0
|
存储
分布式系统的烦恼------《Designing Data-Intensive Applications》读书笔记11
使用分布式系统与在单机系统中处理问题有很大的区别,分布式系统带来了更大的处理能力和存储容量之后,也带来了很多新的"烦恼"。在这一篇之中,我们将看看分布式系统带给我们新的挑战。
1280 0
|
机器学习/深度学习 安全 网络安全
听说Tech Insight在北京城又火了一把?
即将在12月19日国家会议中心举办的Tech Insight是2017年最后一场,也是全年的压轴之作。此前Tech Insight已在北京、上海、广州、深圳、成都等地多次举办,场场爆满。本次北京站结合了历届Tech Insight现场用户反馈的需求,议程设置更加丰满。
2591 0