前言 
之前的文章介绍了网络封装、组件化、基础工具等,有兴趣的可以查看:
写一个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