重学Android Jetpack(十)—使用DataStore替代SharedPreferences

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

简介

Jetpack DataStore是一种数据存储解决方案,它使用Protocol Buffers(Kotlin的反射信息存储也是这样格式)协议存储键值对或类型化对象。DataStore 使用Kotlin协程和Flow以异步、一致的事务方式存储数据。这里要注意的是,DataStore比较适合简单的小型数据存储,不支持部分更新或参照完整性,如果我们需要支持大型或复杂的数据、部分更新或参照完整性,请考虑使用Room,而不是DataStore

SharedPreferences和DataStore的比较

很显然DataStore的出现就是要代替SharedPreferences的使用,那么DataStore对于SharedPreferences的优势在哪里呢?我们先看SharedPreferences存在的一些问题:

SharedPreferences的不足
  • SharedPreferences在存储稍微大点的文件会存在阻塞UI线程而导致ANR,而且不可以在跨进程使用。(说起这个,小弟刚来目前的公司时发现公司的项目有在跨进程使用SharedPreferences,项目看似运行正常,后面在用户使用反馈分析中得知一些偶现的问题正是因为跨进程使用SharedPreferences导致的,只能说真是坑啊…)。

  • 在多线程场的情况使用的效率会比较低,这是因为在get操作的时会锁定SharedPreferencesImpl里面的对象,互斥其他操作,而当 put、commit()apply()操作的时候都会锁住Editor的对象。

  • 每次加载文件的时候都会把数据加载到内存中,而数据会一直存在内存中,会造成内存浪费。如果文件稍大,读取也会比较慢,还会引发程序的频繁gc,引起卡顿的问题。

  • 通过SharedPreferences进行数据保存时无法获取数据存储成功的回调。

DataStore的优势
  • DataStore的设计旨在代替SharedPreferences,官方也建议把数据迁移到 DataStore

  • DataStore是基于Kotlin协程开发的,并支持Flow,所以它在主线程操作是安全的。

  • DataStore提供了两种实现:

    • Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
    • Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求我们使用Protocol Buffers格式协议来定义架构,但可以确保类型安全。
  • DataStore以事务方式处理更新数据,事务有四大特性:原子性、一致性、 隔离性、持久性。

所以,使用DataStore来存储数据会是一种更优的方案。

Preferences DataStore 与 Proto DataStore 区别
  • Preferences DataStore是根据键访问xml文件存储的数据,无需事先定义架构,解决了SharedPreferences的不足;
  • Proto DataStore使用protocol buffers协议来定义架构,可持久保留强类型数据,并保证类型安全,与xml存储相比protocol buffers协议存储速度更快、规格更小、使用更简单,并且更清楚明了,但需要学习新的序列化机制。

DataStore的使用

Preferences DataStore使用

依赖:

 implementation "androidx.datastore:datastore-preferences:1.0.0"

定义Preferences DataStore文件名和存储数据的key:

const val PDS = "pds"

val KEY_USER_NAME = stringPreferencesKey("userName")

创建Preferences DataStore

我们声明一个DataStoreExt.kt的文件来创建,类似顶层函数,方便全局调用:

val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = PreferencesConstant.PDS)

在ViewModel中声明存储和获取数据的方法:

class MainViewModel : ViewModel() {

    val preLiveData = MutableLiveData<String>()

    //存储数据
    fun putValue(dataStore: DataStore<Preferences>, content: String, key: Preferences.Key<String>) {
        viewModelScope.launch(Dispatchers.IO) {
            dataStore.edit { settings ->
                settings[key] = content
            }
        }
    }

    //获取数据
    fun getValue(dataStore: DataStore<Preferences>,key: Preferences.Key<String>) {
        viewModelScope.launch(Dispatchers.IO) {
            dataStore.edit { settings ->
                val text = settings[key]
                preLiveData.postValue(text)
            }
        }
    }
    
      /**
     * 清楚所有键
     */
    fun clearPreferences(dataStore: DataStore<Preferences>){
        viewModelScope.launch {
            dataStore.edit {
                it.clear()
            }
        }
    }
}

Activity中通过ViewModel来使用:

class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel

    private val btnPut: Button by lazy {
        findViewById(R.id.btn_put)
    }

    private val btnGet: Button by lazy {
        findViewById(R.id.btn_get)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProvider(this)[MainViewModel::class.java]

        btnPut.setOnClickListener {
            viewModel.putValue(dataStore,"张三",PreferencesConstant.KEY_USER_NAME)

            Toast.makeText(this,"已存储数据",Toast.LENGTH_SHORT).show()
        }

        btnGet.setOnClickListener {
            viewModel.getValue(dataStore,PreferencesConstant.KEY_USER_NAME)
        }

        viewModel.preLiveData.observe(this){

            Toast.makeText(this,it,Toast.LENGTH_SHORT).show()
        }
    }
}

效果:

QQ图片20220623221407.gif

使用Proto DataStore存储数据

依赖
plugins {
    id "com.google.protobuf" version "0.8.12"
}

dependencies {
    implementation "androidx.datastore:datastore:1.0.0"
    //protobuf
    implementation  "com.google.protobuf:protobuf-javalite:3.14.0"
}

protobuf {
    protoc {
        // 这里设置protoc的版本要跟protobuf-javalite的版本一致
        artifact = 'com.google.protobuf:protoc:3.14.0'
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option "lite"
                }
            }
        }
    }
}

预定义架构创建

Proto DataStore的实现是使用DataStore和协议缓冲区(Protocol Buffers)将类型化的对象保留在磁盘上。

要使用Proto DataStore我们首先要利用协议缓冲区定义架构,通过使用协议,DataStore可以知道存储的类型,并且无需使用键便能提供类型。

新建一个proto的文件夹,路径是:

1656004036771.png

定义架构的代码protobuf语言,这里是照着官方做的,这个语言暂时也不是很熟悉就不展开细讲了,理解基本的也不影响我们使用,我们定义一个user_prefs.proto文件,里面代码如下:

syntax = "proto3";

option java_package = "com.qisan.datastoredemo";
option java_multiple_files = true;

message UserInfoPreference {
  // 下面定义分别是 字段 类型 名称 编号 也就是我们定义实体中的元素
  string name = 1;
  int32 age = 2;
}

接着创建一个与预定义结构中数据结构相同的数据实体:

data class UserInfo(
    val name: String = "张三",
    val age: Int = 23
)

创建Serializer

object UserInfoSerializer : Serializer<UserInfoPreference> {

    override suspend fun readFrom(input: InputStream): UserInfoPreference {
        try {
            return parseFrom(input)
        } catch (exception: Exception) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: UserInfoPreference, output: OutputStream) {
        t.writeTo(output)
    }

    override val defaultValue: UserInfoPreference
        get() = UserInfoPreference.getDefaultInstance()
}
创建Proto DataStore

我们还是把它定义在Kotlin文件顶层调用:

const val USER_PB = "userInfo.pb"

val Context.userInfoStore: DataStore<UserInfoPreference> by dataStore(
    fileName = PreferencesConstant.USER_PB,
    serializer = UserInfoSerializer
)
ViewModel添加存储数据和获取数据的方法
class MainViewModel : ViewModel() {

    val userInfoLiveData = MutableLiveData<UserInfoPreference>()

    /**
     * Proto dataSotre put
     */
    fun putProtoValue(dataStore: DataStore<UserInfoPreference>,userInfo: UserInfo) {
        viewModelScope.launch(Dispatchers.IO) {
            dataStore.updateData { user ->
                user.toBuilder().setName(userInfo.name).setAge(userInfo.age).build()
            }
        }
    }

    /**
     *  Proto dataSotre get
     */
    suspend fun getProtoValue(dataStore: DataStore<UserInfoPreference>) {
        dataStore.data.collect{
            userInfoLiveData.postValue(it)
        }
    }
}

Activity中调用:

        btnPut.setOnClickListener {
            viewModel.putProtoValue(userInfoStore,UserInfo("李四",26))
            Toast.makeText(this,"已存储数据",Toast.LENGTH_SHORT).show()
        }

        btnGet.setOnClickListener {
            lifecycleScope.launch {
                viewModel.getProtoValue(userInfoStore)
            }
        }

        viewModel.userInfoLiveData.observe(this){
            Toast.makeText(this,"${it.name} 已经 ${it.age} 岁了",Toast.LENGTH_SHORT).show()
        }

效果:

QQ图片20220624012817.gif

总结

DataStore的基本使用已经介绍完了,Preferences DataStore其实很好理解,跟之前SharePreferences的用法比较相像,Proto DataStore相对来说难一点,又涉及到了protobuf的使用,我对这个语言也一点不理解,但是按照官方的演示架构也是可以使用,复杂的情况下需要要了解一下protobuf。一般开发的情况我们只是用来保存一些诸如token之类的信息,Preferences DataStore是完全可以满足我们的需求。当然,我们也可以去了解一下使用MMKVK框架,小弟觉得这也是一个替代SharePreferences比较好的框架。

猜你喜欢

转载自juejin.im/post/7112486451626901540