Android Jetpack DataStore
原文地址 www.jianshu.com
导语
Jetpack简介及其它组件文章
DataStore就是SharedPreferences(简称SP)的替代品,Google为什么要用DataStore来替代SP呢,因为SP存在着很多问题,我之前在Android SharedPreferences转为MMKV中有详细说明了SP的不足,但是当时的有些观点还有些浅薄,所以使用了MMKV来替代SP,现在我更推荐大家使用DataStore替代SP,下面会详细讲出。
主要内容
- DataStore的基本概念
- DataStore、SP、MMKV对比
- DataStore的基本使用
- DataStore的封装
DataStore的基本概念
Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
所以如果想要使用DataStore,就必须使用Kotlin,因为DataStore用到了flow,flow用到了协程,协程是Kotlin的特性。
但是,如果将DataStore封装起来,那么直接使用Java调用的话,也是可以正常使用的,所以大家也不用担心。
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。
- Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
- Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。
这里我们重点讲Preferences DataStore,这足以够大多数用户使用。
DataStore、SP、MMKV对比
大量文章指出MMKV的性能是多么多么高,但其实MMKV在存大数据的时候,因为MMKV是自行管理一块内存,性能是反而更低的。
我们先来看一下三种方案进行1000次Int值的连续写入耗时:
连续写入1000次Int值耗时
这么一看MMKV快到离谱,但是SP是可以异步写入的,而DataStore是基于协程的,我们关心的卡顿是主流程的流畅,所以我们只需要考虑主线程的耗时,所以会变成这样:
连续写入1000次Int值耗时(异步)
可以看到耗时大大缩减,但是还是比MMKV慢很多,这是Int数据类型的写入,Int数据是很小的,我们换成长字符串,再来看一下效果:
连续写入1000次长字符串耗时(异步)
这时我们会发现,MMKV反而成为那个最慢的,DataStore遥遥领先。
所以我们要注意的是主线程的耗时,而这个数据其实也并不重要,因为在项目中也很少有1000次大数据的写入,而1000次大数据耗时也不过不到1秒,所以选合适的更重要。
DataStore的基本使用
创建 Preferences DataStore
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
name参数是 Preferences DataStore 的名称。类似SP中的name。
将内容写入 Preferences DataStore
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
suspend fun putIntData() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
我们通过intPreferencesKey来定义我们的Key的name以及存储的数据类型,如果是存放String类型就是stringPreferencesKey。
通过settings[EXAMPLE_COUNTER]是可以取到数据的,通过赋值可以用来存数据。
从 Preferences DataStore 读取内容
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
通过这种方式取到的是一个被Flow,通过exampleCounterFlow.first()就可以拿到真实的数据。
在同步代码中使用 DataStore
DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。
Kotlin 协程提供 runBlocking()
协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking()
从 DataStore 同步读取数据。
val exampleData = runBlocking { context.dataStore.data.first() }
对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
context.dataStore.data.first()
// You should also handle IOExceptions here.
}
}
这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。
DataStore的封装
封装
既然我们都使用Kotlin了,那就使用Kotlin的特性扩展函数来实现吧。
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
/**
* Created by 郭士超 on 2022/11/6 15:48
* Describe:DataStoreExt.kt
*/
/**
* 存放数据
*/
fun <T> DataStore<Preferences>.putData(key: String, value: T) {
runBlocking {
when(value) {
is String -> {
putString(key, value)
}
is Int -> {
putInt(key, value)
}
is Long -> {
putLong(key, value)
}
is Float -> {
putFloat(key, value)
}
is Double -> {
putDouble(key, value)
}
is Boolean -> {
putBoolean(key, value)
}
}
}
}
/**
* 取出数据
*/
fun <T> DataStore<Preferences>.getData(key: String, defaultValue: T): T {
val data = when(defaultValue) {
is String -> {
getString(key, defaultValue)
}
is Int -> {
getInt(key, defaultValue)
}
is Long -> {
getLong(key, defaultValue)
}
is Float -> {
getFloat(key, defaultValue)
}
is Double -> {
getDouble(key, defaultValue)
}
is Boolean -> {
getBoolean(key, defaultValue)
}
else -> {
throw IllegalArgumentException("This type cannot be saved to the Data Store")
}
}
return data as T
}
/**
* 清空数据
*/
fun DataStore<Preferences>.clear() = runBlocking { edit { it.clear() } }
/**
* 存放String数据
*/
private suspend fun DataStore<Preferences>.putString(key: String, value: String) {
edit {
it[stringPreferencesKey(key)] = value
}
}
/**
* 存放Int数据
*/
private suspend fun DataStore<Preferences>.putInt(key: String, value: Int) {
edit {
it[intPreferencesKey(key)] = value
}
}
/**
* 存放Long数据
*/
private suspend fun DataStore<Preferences>.putLong(key: String, value: Long) {
edit {
it[longPreferencesKey(key)] = value
}
}
/**
* 存放Float数据
*/
private suspend fun DataStore<Preferences>.putFloat(key: String, value: Float) {
edit {
it[floatPreferencesKey(key)] = value
}
}
/**
* 存放Double数据
*/
private suspend fun DataStore<Preferences>.putDouble(key: String, value: Double) {
edit {
it[doublePreferencesKey(key)] = value
}
}
/**
* 存放Boolean数据
*/
private suspend fun DataStore<Preferences>.putBoolean(key: String, value: Boolean) {
edit {
it[booleanPreferencesKey(key)] = value
}
}
/**
* 取出String数据
*/
private fun DataStore<Preferences>.getString(key: String, default: String? = null): String = runBlocking {
return@runBlocking data.map {
it[stringPreferencesKey(key)] ?: default
}.first()!!
}
/**
* 取出Int数据
*/
private fun DataStore<Preferences>.getInt(key: String, default: Int = 0): Int = runBlocking {
return@runBlocking data.map {
it[intPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Long数据
*/
private fun DataStore<Preferences>.getLong(key: String, default: Long = 0): Long = runBlocking {
return@runBlocking data.map {
it[longPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Float数据
*/
private fun DataStore<Preferences>.getFloat(key: String, default: Float = 0.0f): Float = runBlocking {
return@runBlocking data.map {
it[floatPreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Double数据
*/
private fun DataStore<Preferences>.getDouble(key: String, default: Double = 0.00): Double = runBlocking {
return@runBlocking data.map {
it[doublePreferencesKey(key)] ?: default
}.first()
}
/**
* 取出Boolean数据
*/
private fun DataStore<Preferences>.getBoolean(key: String, default: Boolean = false): Boolean = runBlocking {
return@runBlocking data.map {
it[booleanPreferencesKey(key)] ?: default
}.first()
}
使用
封装完成我们应该如何使用呢?
//第一步先创建一个自己的Application
class MyApplication: Application() {
companion object {
lateinit var instance : MyApplication
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
/**
* Created by 郭士超 on 2022/11/6 16:38
* Describe:AppDataStore.kt
*/
object AppDataStore {
// 创建DataStore
private val MyApplication.appDataStore: DataStore<Preferences> by preferencesDataStore(
name = "App"
)
// DataStore变量
private val dataStore = MyApplication.instance.appDataStore
private fun <T> putData(key: String, value: T) {
dataStore.putData(key, value)
}
private fun <T> gutData(key: String, value: T): T {
return dataStore.getData(key, value)
}
fun clear() {
dataStore.clear()
}
private const val NUMBER = "number"
fun putNumber(number: Int) {
dataStore.putData(NUMBER, number)
}
fun getNumber(): Int {
return dataStore.getData(NUMBER, 0)
}
}
封装好之后,我们在使用的时候,不同的模块使用不同的XxxDataStore,这样可以解耦合,我们将put和get方法放到XxxDataStore中统一管理,方便我们快速定位这个方法都在哪里调用,从而更快定位到问题或者加快开发速度。