关于数据存储方案,尤其是存储简单的数据,之前一直使用SharedPreferences或者MMKV,但是这是新项目,肯定得用Jetpack推荐得DataStore方案了。
前言
DataStore是一种数据存储方案,允许使用协议缓冲区存储键值对或者类型化得对象,DataStore使用kotlin协程和Flow以异步、一致得事务方式存储数据。
从这个非常官方的介绍我们可以得知一个很关键的点就是异步,而且使用的是协程和Flow,关于Flow这里我也不太熟悉,但是我们需要知道在MVVM架构中是数据驱动,那我就需要从数据下手,如果数据是Flow或者LiveData格式的,那我就可以观察它,做出UI的响应,同时使用的是协程,使用协程以及协程范围的好处就是处理耗时操作时在必要时刻可以取消。
所以我如果能以协程的方式,获取到数据是观察者类型,那就完美了,所以推荐本地小型数据存储就是DataStore了。
其中DataStore提供2种实现,分别是Preferences DataStore和Proto DataStore,其中前者使用键存储和访问数据,后者将数据作为自定义数据类型存储,相当复杂,反正看了官方文档没咋看懂,一般使用就用Preferences DataStore就够了。
Preference DataStore
先是添加依赖:
api("androidx.datastore:datastore-preferences:1.0.0-rc02")
先看官方文档,Preferences DataStore使用DataStore和Preferences类将简单的键值对保存在磁盘上,创建Preferences DataStore在文档中推荐使用顶层函数,使用Context的扩展属性,直接看一下:
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
有了DataStore实例,就开始增加或者删除,将类容写入:
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
和SP一样,这个key在DataStore中也需要分类型,比如int类型就是intPreferencesKey,String类型就是stringPreferencesKey,然后就是这个函数是挂起函数,说明需要在IO线程下在协程里调用。
再看读取数据:
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
这里注意点就是之前说的会返回一个Flow,这也是我们期望的数据类型,其他也没有啥要注意的,就是这个不同类型的key需要用不同类型的PreferencesKey即可。
项目中使用
项目中保存键值对的使用场景很多,就比如保存用户名这个简单的功能,首先就是要封装一下,搞个单例,把读写操作都放在一块,这样代码看起来更简洁:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "wayeal_yun")
class DataStoreManager private constructor(){
private val store = BaseApplication.mBaseApplication.dataStore
companion object{
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager() }
}
suspend fun saveString(key: String,value: String){
Log.i(TAG, "saveString: $key $value")
store.edit {
it[stringPreferencesKey(key)] = value
}
}
suspend fun getString(key: String, default: String): Flow<String> {
Log.i(TAG, "getString: $key")
return store.data.map {
it[stringPreferencesKey(key)] ?: default
}
}
}
- 这里直接使用App的Context即可。
然后在登录成功后进行保存用户名:
//保存用户名
suspend fun saveLoginName(name: String) {
Log.i(TAG, "saveLoginName: $name")
DataStoreManager.instance.saveString(Constant.EDIT_LOGIN_USER_NAME, name)
}
当然这个操作默认是在子线程中执行,同时在ViewModel的协程Scope中,耗时操作都必须要有这个习惯。然后下一次读取时:
//获取用户名
suspend fun getLoginName(): Flow<String> {
Log.i(TAG, "getLoginName: ")
return DataStoreManager.instance.getString(Constant.EDIT_LOGIN_USER_NAME, "11")
}
在loginRepository中获取用户名,之前也说了这是个异步操作,会返回一个Flow,所以在ViewModel中进行捕获:
loginRepository.getLoginName().first {
userName.value = it
true
}
这样就会得到保存在本地的用户名,然后把用户名值赋值给userName这个和xml绑定的liveData,这样界面就能显示出来了,大功告成。
LiveData+DataStore
前面说了我可以获取和保存键值对,但是还是比较麻烦,假如有一个LiveData它可以和DataStore结合起来,也就是setValue的时候把值保存到本地,获取也从本地,这样就可以在ViewModel中少写很多保存和获取逻辑了,直接开干:
class DataStoreLiveData<T>(val key: String,
val default: T, )
: MutableLiveData<T>(){
override fun setValue(value: T?) {
super.setValue(value)
runBlocking {
when(value){
is String -> DataStoreManager.instance.saveString(key,value as String)
is Boolean -> DataStoreManager.instance.saveBoolean(key,value as Boolean)
else -> {
Log.i(TAG, "setValue: error $value")
}
}
}
}
override fun getValue(): T? {
var value: T? = null
runBlocking {
value = when(default){
is String -> DataStoreManager.instance.getString(key,default).first() as T
is Boolean -> DataStoreManager.instance.getBoolean(key,default).first() as T
else -> {
null
}
}
}
return value
}
}
- 由于DataStore的操作都是协程,所以要启动一个协程来做。
- setValue和getValue是在主线程中进行,但是DataStore是在子线程中进行,如果使用数据绑定的话,我先set一个值,这时会立马get这个值,如果DataStore是异步操作不等待的话,就会造成前面set的值还没有生效,后面就进行了get,所以这里为了保证同步生效,使用runBlocking来启动协程,也就是等DataStore操作成功后再取值。
- 假如这里DataStore很慢,会造成UI卡顿,所以其实不太建议这个做法。
总结
替代SP使用的话,就直接使用Preferences DataStore即可,另外一种太麻烦。同时都是异步操作,而且是协程函数,对MVVM中数据处理和性能优化很有帮助。