前言
之前的文章介绍了网络封装、组件化、基础工具等,有兴趣的可以查看:
写一个MVVM快速开发框架(三)单Activity+多Fragment模式
还有一些关于UI的基础工具:
不知道从什么说起,就记录一些关于数据的那些事吧,与数据打交道是我们工作中最常见的事情。
这里主要介绍了一些开发中与数据有关工具,大家可以自己动手实践,也可以查看mvvm_develop
- 事件通信工具
- key-value存储工具
- 数据库
- 网络缓存实现
事件通信总线
为什么要从事件通信说起?最开始的mvc模式开发,数据与UI混杂在一起,数据库和网络请求这些都乱糟糟的,自从EventBus出现之后,给Android开发带来不一样的思路,采用观察者/订阅者模式来处理数据通信。
历史中的事件通信框架:
EventBus
Android事件发布/订阅框架,通过解耦发布者和订阅者简化Android事件传递,这里的事件可以理解为消息。事件传递既可以用于Android四大组件间通讯,也可以用于异步线程和主线程间通讯等。
RxBus
收益与RxJava强大的异步处理能力,我们可以轻易生成一个观察者模式的事件通信框架。也是我之前项目中用到最多的。
LiveData
LiveData不是为了事件通信而生的,但是其天生的属性(可观察属性和生命周期监听属性)我们很难不利用,同样其配合viewModel+Lifecycle似乎是我们mvvm模式下的不二之选。
至于三者之间的优缺点,参考:tech.meituan.com/2018/07/26/…
使用LiveData封装一个事件通信工具
LiveData天生具有生命周期监听和可观察属性,只需要10行核心代码就可以完成:
object LiveDataBus{
private val bus: MutableMap<String, MutableLiveData<Any>> = HashMap()
fun <T> getChannel(target: String, type: Class<T>): MutableLiveData<T> {
if (!bus.containsKey(target)) {
bus[target] = MutableLiveData()
}
return bus[target] as MutableLiveData<T>
}
}
发布消息:
LiveDataBus.getChannel<String>("test").postValue("hahahah")
观察消息:
LiveDataBus.getChannel<String>("test").observe(viewLifecycleOwner,Observer {
xLog.d(it)
})
我们只是创建了一个Map管理LiveData,至此一个跨组件的事件通信组件就完事了,是不是很简单!🤣
但是其还有一些缺点,比如先发布消息,后创建的订阅者依旧能收到消息,以及重复收到消息等,是不是头疼😎
我们可以自定义LiveData实现黏性事件和重复性事件的管理。 这里可以参考大佬们的代码,具体代码也就一个类: github.com/fmtjava/Liv…
优雅的数据处理
我们看一下Google推荐的应用架构指南:
整个架构无非就是围绕数据
和View
去构建,想要优雅的处理数据,我们需要关注几个点:
- 小数据,key-value存储
- 大数据,数据库存储
- 离线缓存与网络请求数据协同
- 数据依赖注入
- 数据与View的绑定
以及其他:
- 文件管理
我们接下来一步一步探索吧
Key-Value存储
我们一般用这种方式存储一些临时数据和配置文件,以键值对为存储方式。
Google提供的有SharedPreferences,sp是我们常用的了,sp缺点很多:性能差、线程不安全,所以有了MMKV的替代方案。
目前我经常使用的就是mmkv的方案,这个网上也有很多介绍的了,官方文档对其原理和使用介绍的非常清除,我们只需要封装一下就可以愉快的使用了。
import android.app.Application
import android.os.Parcelable
import com.tencent.mmkv.MMKV
import java.util.*
/**
* @ClassName mmkvUtils
* @Description mmkv存储工具类,参考:https://github.com/Tencent/MMKV/wiki/android_tutorial_cn
* @Author AlexLu_1406496344@qq.com
* @Date 2020/12/14 16:06
*/
enum class MMKV_TYPE{
USER,
APP
}
class MMKVUtil {
companion object{
@JvmField
val instance = MMKVUtil()
//对于app和用户可以设置不同的mmkv
@Volatile
var mmkv: MMKV ?= null
//用户相关
private val userMMKV by lazy {
MMKV.mmkvWithID("user",MMKV.MULTI_PROCESS_MODE)
}
//app配置相关
private val appMMKV by lazy {
MMKV.mmkvWithID("app",MMKV.MULTI_PROCESS_MODE)
}
//初始化
fun init(app:Application){
MMKV.initialize(app)
mmkv = appMMKV
}
@Synchronized
fun get(type: MMKV_TYPE):MMKVUtil = instance.apply{
mmkv = when(type){
MMKV_TYPE.USER -> {
userMMKV
}
MMKV_TYPE.APP -> {
appMMKV
}
}
}
}
/*-------------Encode----------*/
fun encode(key: String, value: Any?) {
when (value) {
is String -> mmkv?.encode(key, value)
is Float -> mmkv?.encode(key, value)
is Boolean -> mmkv?.encode(key, value)
is Int -> mmkv?.encode(key, value)
is Long -> mmkv?.encode(key, value)
is Double -> mmkv?.encode(key, value)
is ByteArray -> mmkv?.encode(key, value)
is Nothing -> return
}
}
fun <T : Parcelable> encode(key: String, t: T?) {
if(t == null){
return
}
mmkv?.encode(key, t)
}
fun encode(key: String, sets: Set<String>?) {
if(sets ==null){
return
}
mmkv?.encode(key, sets)
}
/*------------Decode-----------*/
fun decodeInt(key: String): Int? {
return mmkv?.decodeInt(key)
}
fun decodeString(key: String): String?{
return mmkv?.decodeString(key)
}
fun decodeDouble(key: String): Double? {
return mmkv?.decodeDouble(key)
}
fun decodeLong(key: String): Long? {
return mmkv?.decodeLong(key)
}
fun decodeBoolean(key: String): Boolean? {
return mmkv?.decodeBool(key)
}
fun decodeFloat(key: String): Float? {
return mmkv?.decodeFloat(key)
}
fun decodeByteArray(key: String): ByteArray? {
return mmkv?.decodeBytes(key)
}
fun <T : Parcelable> decodeParcelable(key: String, tClass: Class<T>): T? {
return mmkv?.decodeParcelable(key, tClass)
}
fun decodeStringSet(key: String): Set<String>? {
return mmkv?.decodeStringSet(key, Collections.emptySet())
}
/*------------Delete-----------*/
//删除key
fun removeKey(key: String) {
mmkv?.removeValueForKey(key)
}
//删除所有
fun clearAll() {
mmkv?.clearAll()
mmkv = null
}
}
需要注意的点:对于app参数和用户参数应该使用不同的mmkv文件,这里通过类型MMKV_TYPE设置,你也可以自行修改
使用步骤:
- application中初始化:
MMKVUtil.init(this)
- 写入
写入app mmkv文件
MMKVUtil.get(MMKV_TYPE.APP).encode(key,"this is mmkv_app params")
写入user mmkv文件
MMKVUtil.get(MMKV_TYPE.USER).encode(key,"this is mmkv_user params")
- 读取:
MMKVUtil.get(MMKV_TYPE.APP).decodeString(key)
- 删除:
全部删除:
MMKVUtil.get(MMKV_TYPE.USER).clearAll()
删除一个key:
MMKVUtil.get(MMKV_TYPE.APP).removeKey(key)
数据库:
很早之前的SQLite异常难用,之后Google推出了Room,Room是jetpcak组件最早的其中之一,其本身是对SQLite的一些封装,能够使我们更流畅的增删改查
快速了解Room
Room架构图:
其使用方法也很简单,主要是三步:
- 给Bean类添加
@Entity
注解,生成表文件 - 通过
@Dao
注解设置数据获取接口 - 通过
@DataBase
注解生成数据库类
对于单一module,我们只需创建一个数据库即可,通过单例创建避免消耗过多资源:
@Database(entities = Test::class,version = 1)
abstract class Database : RoomDatabase(){
companion object {
const val dbName = "Home.db"
@Volatile
private var INSTANCE: Database? = null
fun getInstance(): Database = INSTANCE ?: synchronized(this) {
buildDatabase().also { INSTANCE = it }
}
private fun buildDatabase() =
Room.databaseBuilder(
BaseApp.getContext(),
Database::class.java, dbName)
.addMigrations(MIGRATION_1_2)
.build()
}
}
关于具体的使用方法请参考:
Room官方文档:
developer.android.com/jetpack/and…
Room Demo:
github.com/android/arc…
Android Jetpack ROOM数据库用法介绍:
juejin.cn/post/684490…
踩坑指南:
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
room不能一张表中的字段不能直接保存数据,可以通过gson转String再存储,或者创建多个表格通过关键字连接 参考: juejin.cn/post/684490…
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
数据库升级问题,参考:www.jianshu.com/p/fae0245cf…
数据库调试:
高版本的AndroidStudio可以直接通过App Inspection
查看,如下:
或者直接通过File Explorer
文件管理器导出.db数据库文件,其路径为:/data/data/com.example.package/databases
网络请求缓存
我查看了下手机上目前大部分APP都没有很好的缓存策略,对于有些APP确实没必要缓存,但是对于
掘金APP
首页全是文章博客,浏览体验要求比较高的app竟然没有很好的缓存策略,断网的情况下基本就是显示网络错误
,沸点
的缓存是还是很久很久以前的数据。
目前网上关于网络缓存的介绍都是在HttpClient
中添加自定义Interceptor
,如下:
- 设置一个缓存文件:
//添加Cache拦截器,有网时添加到缓存中,无网时取出缓存
var file: File = File(FileUtil.getAppCachePath())
var cache = Cache(file, 1024 * 1024 * 100)
private val okHttpClientBuilder: OkHttpClient.Builder by lazy {
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.cache(cache)
.addInterceptor(getLogInterceptor())
.addInterceptor(getCacheInterceptor())
}
- 自定义
CacheInterceptor
:
/**
* 设置缓存
*/
private fun getCacheInterceptor():Interceptor = Interceptor { chain ->
var request = chain.request()
//当没有网络时
if (!isNetworkConnected()) {
request = request.newBuilder() //CacheControl.FORCE_CACHE; //仅仅使用缓存
//CacheControl.FORCE_NETWORK;// 仅仅使用网络
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val proceed = chain.proceed(request)
if (isNetworkConnected()) {
//有网络时
proceed.newBuilder() //清除头信息
.header("Cache-Control", "public, max-age=" + 60)
.removeHeader("Progma")
.build()
} else {
//没网络时
val maxTime = 4 * 24 * 60 * 60 //离线缓存时间:4周
proceed.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=$maxTime")
.removeHeader("Progma")
.build()
}
}
/**
* 网络状态判断
*/
private fun isNetworkConnected(): Boolean {
val mConnectivityManager = BaseApp.getContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val mNetworkInfo = mConnectivityManager.activeNetworkInfo
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable
}
return false
}
这种方法只能缓存GET请求,因为GET一般用来获取数据,其他涉及加密和操作的没有必要缓存
我的想法是通过数据库缓存,网络请求的时候自动缓存返回成功的数据,再次请求失败的时候返回上一次请求成功的数据,简单的实现就是手动判断接口请求状态来返回数据:
private val homeDao = HomeDatabase.getInstance().homeDao()
/**
* ROOM数据库与网络请求结合使用
*/
fun getHomeArticle(page:Int,resultLiveData: ResultLiveData<Article>){
launch(
block = {
mService.getHomeArticle(page)
},
response = {
if (it.state == NetState.STATE_SUCCESS){
//数据存入数据库
it.data?.let { article ->
homeDao.apply {
//删除上一次数据
deleteAll()
//插入新的数据
insert(article)
}
}
}
if (it.state == NetState.STATE_ERROR){
//从数据库读取上一次数据
it.data = homeDao.getAllData()[0]
}
resultLiveData.postValue(it)
}
)
}
这里关于网络请求的封装请查看第一篇文章:写一个MVVM快速开发框架(一)基础类封装
但是这样子每一个请求都需要手动设置,而且会涉及不同的数据结构,确实很麻烦。
思路一:将返回数据转为json统一保存
一般返回数据都有一层包装,如下:
class ApiResponse<T> : Serializable {
var code : Int = 0
var data : T ?= null
var message : Any ?= null
var state : NetState = NetState.STATE_UNSTART
var error : String = ""
}
val json = Gson().toJson(response)
val response = Gson().fromJson(json,ApiResponse<Article>().javaClass)
因为这里用到了泛型,如果直接用Gson解析会得到如下报错:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xlu.module_tab1.bean.Article
正确姿势如下:
val type: Type = object : TypeToken<ApiResponse<Article>>() {}.type
val response: ApiResponse<Article> = Gson().fromJson(json, type)
我们在Base模块中创建一个基础数据库,创建NetCacheDao用来专门存储网络缓存:
@Entity(tableName = "NetCache")
data class NetCache(
@PrimaryKey
val md:String,
val response:String
)
md
代表网络请求地址的md5值,response
代表网络返回数据转json的数据
所以最后代码如下:
fun test(){
launch(
block = {
mService.getHomeArticle(1)
},
response = {
val md = "test"
when(it.state){
NetState.STATE_SUCCESS -> {
val json = Gson().toJson(it.data())
val netCache = NetCache(md,json)
cacheDao.insert(netCache)
}
NetState.STATE_FAILED,NetState.STATE_ERROR -> {
val netCache = cacheDao.query(md)
val type: Type = object : TypeToken<Article>() {}.type
it.data = Gson().fromJson(netCache.response, type)
}
}
}
)
}
虽说完成了网络数据缓存的任务,但总感觉不太完美,如果大家有好的思路可以提出来。
最后附上mvvm_develop项目地址,文章代码略有缺失,完整请查看demo,项目整体还在完善中,欢迎大佬们指点。卑微Androider在线求个Star😅
欢迎大家点赞关注和提出问题,个人博客:BugMaker