当前位置: 首页 > news >正文

Android上的基于协程的存储框架

在Android上,经常会需要持久化本地数据,比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架,结合协程,我们可以方便地实现一个轻量级的响应式存储框架。

在使用的场景上,我们使用Key-Value的场景很多,而且我们往往不仅仅是存储数据、获取数据,经常还有需要序列化存储、加密存储、订阅数据的变化的功能。

订阅数据的变化,常见的就是使用发布/订阅模式来实现。

但是使用类如EventBus和RxBus并不是一个好的实践,EventBus没有做适当的封装被滥用的话,会导致逻辑混乱,难以跟踪,并且调试起来也相当困难。

谷歌的DataStore就是一个很好的实现。除了DataStore,我们其实也可以使用基于现有的SharePreference、MMKV通过协程等来实现我们的响应式存储框架。

下面我们就来设计这个存储框架。

首先我们基于我们的功能来定义我们的接口

我们的功能如下

  1. 1.支持存储和读取
  2. 2.支持加密和解密
  3. 3.支持序列化和反序列化
  4. 4.支持多“仓库"

由此我们定义了3组接口

  1. 1.Storage 存储器
  2. 2.Serializer 序列化器
  3. 3.CryptoHandler 加密和解密处理器

在清洁架构的分层中,存储(Storage)是属于一种"接口适配器",因为它为应用的内部业务逻辑(即领域层)提供了与外部世界(即数据库、网络、文件系统等)的接口。一般在Respository中和这些接口适配器进行通讯来获取和存储数据,所以在设计Storage的时候,我们应该遵循下面的概念。

Storage接口定义了一个抽象的存储协议,不关注具体的实现方式,例如使用SharedPreferences,MMKV,或者DataStore,这正是适配器层的职责。通过适配器层,我们可以使得业务逻辑从具体的技术细节中解耦,使其更关注于应用的业务规则,而不是底层的存储细节。

同时,我们的设计要允许我们根据需要,灵活地更换或者修改存储的具体实现,而无需改动业务逻辑或者其他部分的代码。

而这正是清洁架构的一个重要原则:独立性和隔离变化,即依赖抽象而不是具体实现。

基于此设计如下的存储器接口

interface Storage {fun put( key:String, obj:Any?)operator fun <T> get( key: String, classOfT:Class<T>):T?operator fun <T> get( key: String, typeOfT: Type):T?fun contain( key: String):Booleanfun onKeyChanged( key:String): Flow<String>fun remove( key: String)fun removeAllPrefix( prefixKey:String )fun removeExcludePrefix( vararg prefixKey: String )fun clear()
}
inline operator fun <reified T> Storage.get(key: String): T? {return get(key, T::class.java)
}

Storage接口设计将基本的存储操作抽象化,并通过onKeyChanged提供了数据变化的通知,这是一个非常有用的功能,使得可以对存储数据的改变进行反应。

此外,removeAllPrefix和removeExcludePrefix方法也为更精细的数据控制提供了可能性,这在处理具有特定前缀键值对的场景中非常有用。

Storage接口设计的目的是为了隐藏实现细节和提高代码的可读性、可维护性和可扩展性。

下面我们基于此继续扩展我们的Storage功能

首先,我们的数据我们希望是序列化存储的,并且可以支持加密。

因此我们继续定义接口:


interface Serializer {fun serialize(obj: Any): Stringfun <T> deserialize(obj: String, classOfT: Class<T>): Tfun <T> deserialize(obj: String, typeOfT: Type): T
}
inline fun <reified T> Serializer.deserialize(obj: String): T = deserialize(obj, T::class.java)

然后是加密和解密接口:

interface CryptoHandler {fun encrypt(obj: String): Stringfun decrypt(obj: String): String
}

接下来我们就可以使用这两个接口来执行序列化、反序列化,加密和解密的操作。

首先MMKV是支持加密的,但是MMKV使用的是AES CFB-128加密算法来做的。但是它并不是那么足够安全,它没有提供硬件级别的安全加密方法。所以可以考虑自己使用Android KeyStore 来实现硬件级别的加密。

使用Android Keystore来实现,一般大致思路就是拿使用Android的keystore 创建一组加密对密钥,然后使用AES算法来加密和解密。

序列化我们可以使用ProtoBuf或者是json来实现

下面简单使用gson来实现我们的序列化存储如下:

@Singleton
open class JsonSerializer(private val gson: Gson) : Serializer {override fun serialize(obj: Any): String {return gson.toJson(obj)}override fun <T> deserialize(obj: String, classOfT: Class<T>): T {return gson.fromJson(obj, classOfT)}override fun <T> deserialize(obj: String, typeOfT: Type): T {return gson.fromJson(obj, typeOfT)}
}

定义好了接口,实现起来就很简单了,只需要在修改key-value的时候,发送一个key被修改的消息到一个flow,对flow的订阅者就可以订阅数据的改变了。

接下来我们基于MMKV和SharePreference来实现这个存储接口

首先我们来使用SharePreference和MMKV来实现这个存储功能
 

class SharePreferenceStorage (private val context: Context,private val storageType: StorageType,private val serializer: Serializer,private val eventLogger: StorageLogger?,private val cryptoHandler: CryptoHandler?):Storage{private val sharedPreferences: SharedPreferences =context.getSharedPreferences(storageType.alias, Context.MODE_PRIVATE)private val keyChangedFlow = MutableSharedFlow<String>(replay = 100)override fun put(key: String, obj: Any?) {obj?.let {data->sharedPreferences.edit().let {editor->editor.putString( key , serializer.serialize( data ).let {cryptoHandler?.encrypt( it )?:it} )editor.apply()keyChangedFlow.tryEmit( key )eventLogger?.trackEvent(StorageSaveEvent( getStorageName(),key, cryptoHandler != null))}}?: run {remove(key)}}override fun <T> get(key: String, classOfT: Class<T>): T? {sharedPreferences.getString( key ,null  )?.let {cryptoHandler?.decrypt( it )?:it}?.let {eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize( it ,classOfT)}?.let {return it}?:run{return null}}override fun <T> get(key: String, typeOfT: Type): T? {val serializeString =   sharedPreferences.getString( key ,null  )?.let {cryptoHandler?.decrypt( it )?:it}return serializeString?.let {serializer.deserialize( it ,typeOfT)}}override fun onKeyChanged(key: String): Flow<String> {return keyChangedFlow.asSharedFlow()}override fun contains(key: String): Boolean {return sharedPreferences.contains( key )}override fun remove(key: String) {if( contains( key ) ){sharedPreferences.edit().let {editor->editor.remove( key )editor.apply()keyChangedFlow.tryEmit( key )eventLogger?.trackEvent(StorageRemoveEvent( getStorageName(),key))}}}override fun removeAllPrefix(prefixKey: String) {sharedPreferences.all?.let {allData->allData.keys.filter { it.startsWith( prefixKey ) }.forEach {remove( it )}}}override fun removeExcludePrefix(vararg prefixKey: String) {sharedPreferences.all?.let {allData->val prefixSet = prefixKey.toSet()val allKeys = allData.keysallKeys.forEach { key ->if (prefixSet.none { key.startsWith(it) }) {remove(key)}}}}override fun clear() {sharedPreferences.edit().let {editor->sharedPreferences.all.keys.forEach {remove( it )}keyChangedFlow.tryEmit( CLEAR_CACHE )eventLogger?.trackEvent(StorageClearEvent( getStorageName()))}}private fun getStorageName():String{return "SharePreference-${storageType.alias}"}}

下面是基于MMKV的实现:

class MMKVStorage constructor(private val storageType: StorageType,private val serializer: Serializer,private val eventLogger: StorageLogger?,private val cryptoHandler: CryptoHandler?): Storage {private val mmkv: MMKV = MMKV.mmkvWithID( storageType.alias, MMKV.MULTI_PROCESS_MODE)private val keyChangedFlow = MutableSharedFlow<String>(replay = 100)private val subscribeKeyList:MutableList<String> = mutableListOf()override fun put(key: String, obj: Any?) {obj?.let {val serializerObj = serializer.serialize( obj ).let {cryptoHandler?.encrypt( it )?:it}mmkv.encode( key,serializerObj)keyChangedFlow.tryEmit(key)eventLogger?.trackEvent(StorageSaveEvent( getStorageName(),key, cryptoHandler != null))} ?: run{remove(key)}}override fun <T> get(key: String, classOfT: Class<T>): T? {return mmkv.decodeString( key )?.let{ jsonString->eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize(jsonString.let {cryptoHandler?.decrypt(it)?:it},classOfT)}}override fun <T> get(key: String, typeOfT: Type): T? {return mmkv.decodeString( key)?.let { jsonString->eventLogger?.trackEvent(StorageLoadEvent( getStorageName(),key, true))serializer.deserialize( jsonString.let {cryptoHandler?.decrypt(it)?:it}, typeOfT)}}override fun onKeyChanged(key: String): Flow<String> {subscribeKeyList.add(key)return keyChangedFlow.asSharedFlow().filter { it == key }}override fun contains(key: String): Boolean {return mmkv.containsKey( key )}override fun remove(key: String) {mmkv.remove(key).apply()eventLogger?.trackEvent(StorageRemoveEvent( getStorageName(),key))keyChangedFlow.tryEmit( key )}override fun removeAllPrefix( prefixKey:String ){val allKeys = mmkv.allKeys()?.clone()?: emptyArray()allKeys.forEach { if( it.contains(prefixKey)) remove(it) }}override fun removeExcludePrefix(vararg prefixKey: String) {val allKeys = mmkv.allKeys()?.clone() ?: emptyArray()val prefixSet = prefixKey.toSet()allKeys.forEach { key ->if (prefixSet.none { key.startsWith(it) }) {remove(key)}}}override fun clear() {mmkv.allKeys()?.forEach {remove(it)}keyChangedFlow.tryEmit( Storage.CLEAR_CACHE )mmkv.clearAll()eventLogger?.trackEvent(StorageClearEvent( getStorageName()))}private fun getStorageName():String {return "mmkv-${storageType.alias}"}}

通过上面的代码,我们就可以实现订阅数据的改变。

相关文章:

Android上的基于协程的存储框架

在Android上&#xff0c;经常会需要持久化本地数据&#xff0c;比如我们需要缓存用户的配置信息、用户的数据、缓存数据、离线缓存数据等等。我们通常使用的工具为SharePreference、MMKV、DataStore、Room、文件等等。通过使用现有的存储框架&#xff0c;结合协程&#xff0c;我…...

虚拟现实与增强现实技术的商业应用

章节一&#xff1a;引言 随着科技的不断发展&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;与增强现实&#xff08;Augmented Reality&#xff0c;简称AR&#xff09;技术正日益成为商业领域中的重要创新力量。这两种技术为企业带来了前所未…...

每日后端面试5题 第六天

1. Java中有几种类型的流 字符流、字节流 输入流、输出流 节点流、处理流 2 .Spring支持的几种bean的作用域 五种&#xff1a; 1.singleton bean在每个ioc容器中只有一个实例 2.prototype 可以有多个实例 3-5在web环境中才生效 3.request 每次请求才创建bean 4.se…...

LeetCode150道面试经典题-- 两数之和(简单)

1.题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按任意…...

转义字符\

转移字符&#xff0c;就是通过字符&#xff0c;来转变原来字符的意思 常见的转义字符&#xff1a; 1、 2 注&#xff1a;" 的作用和他是类似的 3 4、 当打印\a时&#xff0c;电脑会出现一个警告&#xff0c;蜂鸣的声音 5、 阿斯克码表...

什么是DNS欺骗及如何进行DNS欺骗

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、什么是 DNS 欺骗&#xff1f;二、开始1.配置2.Ettercap启动3.操作 总结 前言 我已经离开了一段时间&#xff0c;我现在回来了&#xff0c;我终于在做一个教…...

Android核心开发之——OpenGL

OpenGL是一种用于编程计算机图形的应用程序编程接口&#xff08;API&#xff09;。它提供了一系列函数和方法&#xff0c;用于绘制2D和3D图形&#xff0c;以及进行渲染和图形处理。OpenGL可以跨平台使用&#xff0c;支持各种操作系统和硬件设备。它被广泛应用于游戏开发、虚拟现…...

公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试

公共服务领域:西安新小区业主自立业主委员会年底分红83万以及103万事件区块链资金透明监管与投票解决方案的尝试 作者 重庆电子工程职业学院 | 向键雄 杜小敏 前言 本项目想法来源于,西安新小区业主开出物业自立业主委员会年底分红83万以及103万事件,对于此类事件,我们刨…...

ID3 决策树

西瓜数据集D如下: 编号色泽根蒂敲声纹理脐部触感好瓜1青绿蜷缩浊响清晰凹陷硬滑是2乌黑蜷缩沉闷清晰凹陷硬滑是3乌黑蜷缩浊响清晰凹陷硬滑是4青绿蜷缩沉闷清晰凹陷硬滑是5浅白蜷缩浊响清晰凹陷硬滑是6青绿稍蜷浊响清晰稍凹软粘是7乌黑稍蜷浊响稍糊稍凹软粘是8乌黑稍蜷浊响清晰…...

简单线性回归:预测事物间简单关系的利器

文章目录 &#x1f340;简介&#x1f340;什么是简单线性回归&#xff1f;&#x1f340;简单线性回归的应用场景使用步骤&#xff1a;注意事项&#xff1a; &#x1f340;代码演示&#x1f340;结论 &#x1f340;简介 在数据科学领域&#xff0c;线性回归是一种基本而强大的统…...

Vue2-收集表单数据、过滤器、内置指令与自定义指令、Vue生命周期

&#x1f954;&#xff1a;我徒越万重山 千帆过 万木自逢春 更多Vue知识请点击——Vue.js VUE2-Day4 收集表单数据1、不同标签的value属性2、v-model的三个修饰符 过滤器内置指令与自定义指令1、内置指令2、自定义指令定义语法&#xff08;1&#xff09;函数式&#xff08;2&am…...

正则表达式学习详解

正则表达式 正则表达式&#xff08;Regular Expression&#xff09;&#xff0c;通常简称为正则或正则表达式&#xff0c;是一种用于描述字符串模式的工具。它是由一系列字符和特殊字符组成的字符串&#xff0c;用于定义搜索模式或进行字符串匹配、替换、提取等操作。 正则表…...

工具箱:在线免费使用的文档工具:(PDF转换,图片压缩等)

这些都是博主亲自使用过的&#xff0c;可以使用。 PDF转换器&#xff1a; http://www.pdfdo.com/ 图片压缩&#xff1a; 免费在线图片/视频压缩工具 | 图片压缩 | 免费 JPG PNG GIF 图像压缩 (yalijuda.com) 文档OCR转EXCEL: 文字识别 OCR_ 图片文字识别_图片文字智能识别…...

Qt6之QStackedWidget——Qt仿ToDesk(2)

一、 QStackedWidget概述 QStackedWidget也叫堆栈窗体类&#xff0c;它继承于QFrame&#xff0c;主要与QListWidget等结合使用&#xff0c;实现“一个界面多个页面切换”。 二、QStackedWidget示例 如下图&#xff0c;当点击左边 QListWidget里的菜单时&#xff0c;右边跟随切…...

Harbor企业镜像仓库部署(本地)

简述&#xff1a; Docker 官方镜像仓库是用于管理公共镜像的地方&#xff0c;大家可以在上面找到想要的镜像&#xff0c;也可以把自己的镜像推送上去。但是有时候服务器无法访问互联网&#xff0c;或者不希望将自己的镜像放到互联网上&#xff0c;那么就需要用到 Docker Regis…...

【Linux】如何打包成动静态库,第三方动静态库如何使用?

文章目录 1. 打包成静态库2. 打包成动态库&#xff08;共享库&#xff09;3. 使用第三方静态库4. 使用第三方动态库 5. 动态库的加载6. 注意事项 库的名称&#xff1a;去掉前面的 lib 去掉后面的 .a(版本号) .so(版本号) 剩下的&#xff0c;才是库正真的名称。 查看文件依赖库…...

SAP MM学习笔记20- SAP中的英文2 - SD中英文,日语,中文

SD模块中的英文&#xff0c;日语&#xff0c;中文 对照。 販売管理 日本語英語中国語受注伝票sales order销售订单出荷伝票delivery order交货订单ピッキングリストpicking list领货清单シップメント伝票shipment document发运单据出庫確認post goods issue发货确认請求伝票b…...

计算机网络中的一些基本概念

IP地址: 址用于定位主机的网络地址。是一个32位的二进制数&#xff0c;通常被分割为4个“8位二进制数”&#xff08;也就是4个字节&#xff09;.**端口号:**在网络通信中&#xff0c;IP地址用于标识主机网络地址&#xff0c;端口号可以标识主机中发送数据、接收数据的进程。简单…...

pytest 用例运行方式

一、命令行方式运行 执行某个目录下所有的用例&#xff0c;符合规范的所有用例 进入到对应的目录,直接执行pytest; 例如需要执行testcases 下的所有用例; 可以进入testcases 目录; 然后执行pytest 进入对应目录的上级目录,执行pytest 目录名称/ ; ; 例如需要执行testcases 下…...

简单入门seleniumUI自动化测试

目录 一、selenium的介绍 二、selenium的原理 三、selenium的八种元素定位的方法 1、ID定位&#xff1a; 2 、name定位&#xff1a; 3、class定位&#xff1a; 4、tag定位&#xff1a; 5、link_text定位&#xff1a; 6、partial_link_text定位&#xff1a; 7、css定位…...

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战&#xff1a;腾讯云IM群组成员管理&#xff08;增删改查&#xff09; 一、前言 在社交类App开发中&#xff0c;群组成员管理是核心功能之一。本文将基于UniApp框架&#xff0c;结合腾讯云IM SDK&#xff0c;详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

剑指offer20_链表中环的入口节点

链表中环的入口节点 给定一个链表&#xff0c;若其中包含环&#xff0c;则输出环的入口节点。 若其中不包含环&#xff0c;则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

JDK 17 新特性

#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持&#xff0c;不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的&#xff…...

Caliper 配置文件解析:config.yaml

Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...

企业如何增强终端安全?

在数字化转型加速的今天&#xff0c;企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机&#xff0c;到工厂里的物联网设备、智能传感器&#xff0c;这些终端构成了企业与外部世界连接的 “神经末梢”。然而&#xff0c;随着远程办公的常态化和设备接入的爆炸式…...

C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...

CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝

目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为&#xff1a;一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...