青海省建设网站企业,创建一个网站的一般步骤要点,佛山营销网站开发怎么选,怎样自学设计室内装修效果图写在前面
最近在翻Jetpack库#xff0c;发现了DataStore#xff0c;官方是这么说的#xff1a; Jetpack DataStore 是一种数据存储解决方案#xff0c;允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 …写在前面
最近在翻Jetpack库发现了DataStore官方是这么说的 Jetpack DataStore 是一种数据存储解决方案允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 如果您目前是使用 SharedPreferences存储数据的请考虑迁移到 DataStore。 显而易见在需要存储较小或简单的数据集时DataStore比起SP更加简单且安全性更高所以学习使用DataStore是很有价值的。
基础知识
对比
DataStore的存在是为了替代SP所以为什么可以替代呢我们看看官方给的图来看看SP相对于DataStore有什么劣势。 界面线程上的安全调用 SP的apply() 方法会阻断 fsync() 上的界面线程。每次有服务启动或停止以及每次 activity 在应用中的任何地方启动或停止时系统都会触发待处理的 fsync() 调用。 界面线程在 apply() 调度的待处理 fsync() 调用上会被阻断这通常会导致 ANR。运行时的异常影响 SharedPreferences 会将解析错误作为运行时异常抛出类型安全 例如以下代码我们先写入数据其中设置key所对应的值为int类型但在后面使用相同key获取数据时却调用getString()方法这样程序一旦运行就会报错java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。这段代码在编译阶段完全正常但SharedPreferences却无法对这种操作进行规避需要完全依靠开发者本身去遵循规范。
val sp getSharedPreferences(test, Context.MODE_PRIVATE)
val edit sp.edit()
edit.putInt(key, 0);
edit.apply()
val value sp.getString(key, )注意
在开始DataStore的学习前我们要记住以下几个规则引用自官方文档
请勿在同一进程中为给定文件创建多个 DataStore 实例否则会破坏所有 DataStore 功能。如果给定文件在同一进程中有多个有效的 DataStore 实例DataStore 在读取或更新数据时将抛出 IllegalStateException。DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证都失效并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API 且能够高效进行序列化的协议缓冲区。切勿对同一个文件混用 SingleProcessDataStore 和 MultiProcessDataStore。如果您打算从多个进程访问 DataStore请始终使用 MultiProcessDataStore。
准备
我们通过一个计数器例子在具体的情景中理解和使用DataStore xml布局如下
?xml version1.0 encodingutf-8?
androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools android:layout_widthmatch_parent android:layout_heightmatch_parent tools:context.MainActivity TextView android:idid/tv android:layout_widthwrap_content android:layout_heightwrap_content android:text0 android:textSize28sp app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent / com.google.android.material.floatingactionbutton.FloatingActionButton android:idid/fab android:layout_widthwrap_content android:layout_heightwrap_content android:layout_margin24dp android:srcdrawable/ic_baseline_exposure_plus_1_24 app:backgroundTintcolor/black app:backgroundcolor/black app:tintcolor/white app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent android:contentDescriptionadd / /androidx.constraintlayout.widget.ConstraintLayoutactivity代码如下
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv findViewByIdTextView(R.id.tv) val fab findViewByIdFloatingActionButton(R.id.fab) }
}我们的最终目的是通过不断点击FloatingActionButton使得TextView内的数字不断1 Preferences DataStore
Preferences DataStore 根据键访问数据。虽然不确保类型安全但因为无需事先定义架构Preferences DataStore相对于Proto DataStore更易上手且创建更快。
添加依赖
implementation(androidx.datastore:datastore-preferences:1.0.0) 创建 Preferences DataStore
我们使用 preferencesDataStore 创建 Preferences DataStore 的实例通过 preferencesDataStore 委托可确保我们有一个 DataStore 实例在应用中具有该名称。
private val Context.dataStore: DataStorePreferences by preferencesDataStore(name count-preferences)定义键
由于 Preferences DataStore 不使用预定义的架构我们必须使用相应的键类型函数为需要存储在 DataStorePreferences 实例中的每个值定义一个键。 官方提供以下方法用于键值的定义而键的类型可以通过各方法的命名体现。
intPreferencesKey()doublePreferencesKey()stringPreferencesKey()booleanPreferencesKey()floatPreferencesKey()longPreferencesKey()stringSetPreferencesKey()
在计数器案例中TextView展示的是当前计数值所以我们需要为int值定义一个键即使用intPreferencesKey()
val COUNTER intPreferencesKey(counter)写入数据
在计数器的案例中我们通过 FloatingActionButton 的点击事件来进行DataStore的写入操作而Preferences DataStore的写入操作通过edit函数实现。注意edit函数是一个挂起函数所以我们需要在协程内运行。
fab.setOnClickListener { MainScope().launch { dataStore.edit { preferences - // 获取当前存储在dataStore内key为COUNTER的键值val currentCounterValue preferences[COUNTER] ?: 0 // 将改键值1 preferences[COUNTER] currentCounterValue 1 } }
}读取数据
Preferences DataStore 公开 FlowPreferences 中存储的数据每当偏好设置发生变化时FlowPreferences就会发出该数据。我们使用DataStore.data属性其返回值是Flow所以每当我们点击 FloatingActionButton 修改数据我们能及时接收改变后的数据并修改TextView状态。
MainScope().launch { dataStore.data .map { it[COUNTER] ?: 0 }.collect { tv.text it.toString() }
}从 SharedPreferences 迁移到 Preferences DataStore
为了演示怎么迁移我们重头再来在准备部分的代码基础上临时创建 SharedPreferences 储存数据。
val sp getSharedPreferences(test,Context.MODE_PRIVATE)
val edit sp.edit()
// 为了验证顺利迁移我们初始值设置为10
edit.putInt(number,10);
edit.apply()运行程序后查看数据已经成功保存在本地 现在可以开始将 SharedPreferences 迁移到 Preferences DataStore 了。因为 DataStore 的存在就是为了替代SP所以谷歌早提供SharedPreferencesMigration属性用于SP数据迁移。其他代码与前面类似只需向迁移列表传入 SharedPreferencesMigration属性其中构造函数第二个参数 sharedPreferencesName 为所创建SP的文件名称在本例中即为”test“。
private val Context.dataStore: DataStorePreferences by preferencesDataStore( name preferences-test, // 新增部分produceMigrations { context - listOf(SharedPreferencesMigration(context, test)) }
)由于键只能从 SharedPreferences 迁移一次因此在我们迁移完毕后需要将刚才临时创建的SP相关代码删除此时的完整代码如下 有一处需要注意的在SP文件创建时我们的key值设置为“number”迁移后dataStore的key值也会被设置为“number”。所以与前面的例子相比我们还需要将intPreferencesKey()函数中的key值更改为“number”。
class MainActivity : AppCompatActivity() { // 创建:preferencesDataStore 委托可确保我们有一个 DataStore 实例在应用中具有该名称 private val Context.dataStore: DataStorePreferences by preferencesDataStore( name preferences-test, produceMigrations { context - listOf(SharedPreferencesMigration(context, test)) } ) private val COUNTER intPreferencesKey(number) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv findViewByIdTextView(R.id.tv_test) val fab findViewByIdFloatingActionButton(R.id.fab) MainScope().launch { dataStore.data .map { it[COUNTER] ?: 0 }.collect { tv.text it.toString() } } fab.setOnClickListener { MainScope().launch { dataStore.edit { val currentCounterValue it[COUNTER] ?: 0 it[COUNTER] currentCounterValue 1 } } } }
}运行程序后可以看到界面初始数值为10说明迁移完毕 Proto DataStore
SharedPreferences 和 Preferences DataStore 的一个缺点是无法定义架构保证不了存取键时使用了正确的数据类型。而 Proto DataStore 利用协议缓冲区来定义架构来解决此问题确保了类型安全。 协议缓冲区可持久保留强类型数据。与 XML 和其他类似的数据格式相比协议缓冲区速度更快、规格更小、使用更简单并且更清楚明了。虽然使用 Proto DataStore 需要学习新的序列化机制但因为 Proto DataStore 的强大类型优势所以非常值得我们学习。
添加依赖
添加协议缓冲区插件
plugins { ……id com.google.protobuf version 0.8.17
}添加协议缓冲区和 Proto DataStore 依赖项
dependencies { ……implementation androidx.datastore:datastore:1.0.0implementation com.google.protobuf:protobuf-javalite:3.18.0
}配置协议缓冲区 如果在这步Sync Now报错先Sync Now前两步再Sync Now对协议缓冲区的配置
protobuf { protoc{ // 设置 protoc 的版本号artifact com.google.protobuf:protoc:3.14.0 } generateProtoTasks { all().each { task - task.builtins { java { option lite } } } }
}定义架构
在 app/src/main/ 目录下创建proto文件夹我们将在里面创建proto文件如图所示 创建count.proto文件文件和相关注释内容如下 其中内部类字段类型与Java的对应关系 string-Stringint32-intint64-longbool-Booleanfloat-floatdouble-double
// 声明proto的版本
syntax proto3;option java_package com.wg.jetpackDemos.dataStore.proto; // 指定了生成的Java类的包名
option java_multiple_files true; // 设置生成的Java类是一个文件还是多个文件// message 声明的是内部类
message Count { int32 counter 1;
}每当我们创建或者变更proto文件时都需要Rebuild Project即可生成对应的Java文件
创建 Proto DataStore
创建序列化器 定义一个实现 SerializerT的类其中 T 是 proto 文件中定义的类型。通过实现序列化器告知DataStore如何读取和写入我们在 proto 文件中定义的数据类型如果磁盘上没有数据序列化器还会定义默认返回值。
object CountData : SerializerCount { override val defaultValue: Count get() Count.getDefaultInstance() override suspend fun readFrom(input: InputStream): Count { try { return Count.parseFrom(input) }catch (exception:InvalidProtocolBufferException){ throw CorruptionException(Cannot read proto., exception) } } override suspend fun writeTo(t: Count, output: OutputStream) t.writeTo(output)
}创建 Proto DataStore 实例 使用 dataStore 所创建的属性委托来创建 DataStoreT 实例其中 T 是在 proto 文件中定义的类型。 fileName参数告知 DataStore 使用哪个文件存储数据 serializer 参数告知 DataStore 在第一步中定义的序列化器类的名称
val Context.counterDataStore : DataStoreCount by dataStore( fileName count.pb, serializer CountData
)写入数据
与Preferences DataStore不同Proto DataStore使用updatData()函数用于更新存储的对象。
fab.setOnClickListener { MainScope().launch { counterDataStore.updateData { count -count.toBuilder() .setCounter(count.counter 1) .build() } }
}读取数据
读取数据则与 Preferences DataStore 类似
MainScope().launch { counterDataStore.data.collect { count -tv.text count.counter.toString() }
}从 SharedPreferences 迁移到 Proto DataStore
前期准备与上面“从 SharedPreferences 迁移到 Preferences DataStore”部分相同如果有跳过 Preferences DataStore 部分直接看 Proto DataStore 的朋友需要往回翻看一下。 同迁移到 Preferences DataStore 的思路一样我们只需在DataStore构造器中向迁移列表传入 SharedPreferencesMigration属性。这里需要注意的是SharedPreferencesMigration的包为androidx.datastore.migrations.SharedPreferencesMigration我那时候因为导错包找了好久的bug请以我为戒。
val Context.counterDataStore : DataStoreCount by dataStore( fileName count.pb, serializer CountData, produceMigrations { context - listOf( SharedPreferencesMigration(context,test){ sharedPreferencesView, counter - // 获取 SharedPreferences 的数据 val count sharedPreferencesView.getInt(number,0) counter.toBuilder().setCounter(count).build() } ) }
)在运行前记得删掉SP相关代码迁移完毕结果如下 Android 学习笔录
Jetpack全家桶篇内含Composehttps://qr18.cn/A0gajp Android 性能优化篇https://qr18.cn/FVlo89 Android Framework底层原理篇https://qr18.cn/AQpN4J Android 车载篇https://qr18.cn/F05ZCM Android 逆向安全学习笔记https://qr18.cn/CQ5TcL Android 音视频篇https://qr18.cn/Ei3VPD OkHttp 源码解析笔记https://qr18.cn/Cw0pBD Kotlin 篇https://qr18.cn/CdjtAF Gradle 篇https://qr18.cn/DzrmMB Flutter 篇https://qr18.cn/DIvKma Android 八大知识体https://qr18.cn/CyxarU Android 核心笔记https://qr21.cn/CaZQLo Android 往年面试题锦https://qr18.cn/CKV8OZ 2023年最新Android 面试题集https://qr18.cn/CgxrRy Android 车载开发岗位面试习题https://qr18.cn/FTlyCJ 音视频面试题锦https://qr18.cn/AcV6Ap