前言
之前的文章介绍了网络封装、组件化、基础工具等,有兴趣的可以查看:
写一个MVVM快速开发框架(一)基础类封装
写一个MVVM快速开发框架(二)组件化改造
写一个MVVM快速开发框架(三)单Activity+多Fragment模式
还有一些关于UI的基础工具:
封装一个好看的吐司工具
一点也不炫酷的Navigation动画
不知道从什么说起,就记录一些关于数据的那些事吧,与数据打交道是我们工作中最常见的事情。
这里主要介绍了一些开发中与数据有关工具,大家可以自己动手实践,也可以查看mvvm_develop
事件通信工具
key-value存储工具
数据库
网络缓存实现
事件通信总线
为什么要从事件通信说起?最开始的mvc模式开发,数据与UI混杂在一起,数据库和网络请求这些都乱糟糟的,自从EventBus出现之后,给Android开发带来不一样的思路,采用观察者/订阅者模式来处理数据通信。
历史中的事件通信框架:
EventBus
Android事件发布/订阅框架,通过解耦发布者和订阅者简化Android事件传递,这里的事件可以理解为消息。事件传递既可以用于Android四大组件间通讯,也可以用于异步线程和主线程间通讯等。
RxBus
收益与RxJava强大的异步处理能力,我们可以轻易生成一个观察者模式的事件通信框架。也是我之前项目中用到最多的。
LiveData
LiveData不是为了事件通信而生的,但是其天生的属性(可观察属性和生命周期监听属性)我们很难不利用,同样其配合viewModel+Lifecycle似乎是我们mvvm模式下的不二之选。
至于三者之间的优缺点,参考:https://tech.meituan.com/2018/07/26/android-livedatabus.html
使用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实现黏性事件和重复性事件的管理。
这里可以参考大佬们的代码,具体代码也就一个类: https://github.com/fmtjava/LiveDataBus/blob/master/LiveDataBus/src/main/java/com/fmt/livedatabus/LiveDataBus.kt
优雅的数据处理
我们看一下Google推荐的应用架构指南 :
整个架构无非就是围绕数据
和View
去构建,想要优雅的处理数据,我们需要关注几个点:
小数据,key-value存储
大数据,数据库存储
离线缓存与网络请求数据协同
数据依赖注入
数据与View的绑定
以及其他:
我们接下来一步一步探索吧
Key-Value存储
我们一般用这种方式存储一些临时数据和配置文件,以键值对为存储方式。
Google提供的有SharedPreferences,sp是我们常用的了,sp缺点很多:性能差、线程不安全,所以有了MMKV的替代方案。
目前我经常使用的就是mmkv的方案,这个网上也有很多介绍的了,官方文档 对其原理和使用介绍的非常清除,我们只需要封装一下就可以愉快的使用了。
import android.app.Applicationimport android.os.Parcelableimport com.tencent.mmkv.MMKVimport java.util.*enum class MMKV_TYPE { USER, APP } class MMKVUtil { companion object { @JvmField val instance = MMKVUtil() @Volatile var mmkv: MMKV ?= null private val userMMKV by lazy { MMKV.mmkvWithID("user" ,MMKV.MULTI_PROCESS_MODE) } 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 } } } } 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) } 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()) } fun removeKey (key: String ) { mmkv?.removeValueForKey(key) } fun clearAll () { mmkv?.clearAll() mmkv = null } }
需要注意的点:对于app参数和用户参数应该使用不同的mmkv文件,这里通过类型MMKV_TYPE设置,你也可以自行修改
使用步骤:
application中初始化:
写入
写入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官方文档:
https://developer.android.com/jetpack/androidx/releases/room?hl=zh-cn
Room Demo:
https://github.com/android/architecture-components-samples
Android Jetpack ROOM数据库用法介绍:
https://juejin.cn/post/6844903903020974093#heading-12
踩坑指南:
Cannot figure out how to save this field into database . You can consider adding a type converter for it.
room不能一张表中的字段不能直接保存数据,可以通过gson转String再存储,或者创建多个表格通过关键字连接
参考: https://juejin.cn/post/6844903790793981959
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 .
数据库升级问题,参考:https://www.jianshu.com/p/fae0245cf384
数据库调试:
高版本的AndroidStudio可以直接通过App Inspection
查看,如下:
或者直接通过File Explorer
文件管理器导出.db数据库文件,其路径为:/data/data/com.example.package/databases
网络请求缓存
我查看了下手机上目前大部分APP都没有很好的缓存策略,对于有些APP确实没必要缓存,但是对于掘金APP
首页全是文章博客,浏览体验要求比较高的app竟然没有很好的缓存策略,断网的情况下基本就是显示网络错误
,沸点
的缓存是还是很久很久以前的数据。
目前网上关于网络缓存的介绍都是在HttpClient
中添加自定义Interceptor
,如下:
设置一个缓存文件:
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.new Builder() .cacheControl(CacheControl.FORCE_CACHE) .build() } val proceed = chain.proceed(request) if (isNetworkConnected() ) { proceed.new Builder() .header("Cache-Control" , "public, max-age=" + 60 ) .removeHeader("Progma" ) .build() } else { val maxTime = 4 * 24 * 60 * 60 proceed.new Builder() .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() 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() .to Json(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() .to Json(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