Android MVVM框架搭建(三)MMKV + Room + RxJava2

  • 前言
    • 正文
    • 一、添加依赖
    • 二、MMKV
      • 1. 初始化
      • 2. 数据存取
      • 3. 使用
    • 三、Room
      • 1. @Entity
      • 2. @Dao
      • 3. @Database
      • 4. 初始化
      • 5. 使用
      • 6. 优化
    • 四、RxJava2
      • 1. Flowable&Completable
      • 2. CustomDisposable
      • 3. 使用
    • 五、源码

前言

  在上一篇文章中,我讲述了怎么在MVVM框架中搭建网络访问框架,并通过一个必应的每日壁纸做了一次请求接口的访问演示,这篇文章就需要来讲述Android端的本地数据库的使用和在MVVM中使用方式了。

正文

  本文说的是数据库,为什么要讲这个呢,因为在实际开发中,有一些数据并不需要实时更新,我们只需要在第一次打开应用的时候获取到,然后保存到手机本地数据库中即可,需要的时候从数据库中获取。当数据要更新是再从服务器获取,这样可以减少请求次数。

  而我所讲的是JetPack中的一个组件,Room,这是一个数据库组件,实际上也是对Sqlite的上层封装,在没有Room之前我们也会使用一些第三方的开源库,比如GreenDao、LitePal、ORMLite等。当然了,你现在依然可以使用这些开源库,毕竟你养成了使用习惯了。但是本着技多不压身的原则,我们还是可以了解一下的,你说呢?

一、添加依赖

  在创建的项目里,默认是没有Room的依赖的,因此需要手动去添加,添加在app的build.gradle中的dependencies{}闭包下,代码如下:

 //Room数据库implementation 'androidx.room:room-runtime:2.3.0'annotationProcessor 'androidx.room:room-compiler:2.3.0'//Room 支持RxJava2implementation 'androidx.room:room-rxjava2:2.3.0'//腾讯MMKVimplementation 'com.tencent:mmkv:1.2.11'

然后点击 Sync Now进行项目的同步,同步之后就可以开始使用了。

二、MMKV

  在Android系统中使用了多年的SharedPreferences ,终于被Google给放弃了,在JetPack的新组件中新增了一个DataStore,其实在DataStore出现之前已经有一些第三方的本地缓存处理库了,例如腾讯的MMKV库,比较的好用,在我以往的博客中也没有使用过MMKV,就在本文中使用吧,其实JetPack中也有一个组件是用来解决SharedPreferences的,就是DataStore,但是我发现它的使用群体还没有上去,因此就先不做介绍,改成了MMKV库,这个库个人感觉使用起来比DataStore要简单一些,这同样是一个很实用的库。

  在上面的build.gradle配置中我已经添加了目前最新的依赖库了,下面使用它吧。其实很简单的。

1. 初始化

  第一步就是在自定义的Application中进行初始化,在onCreate方法中增加如下代码:

 //MMKV初始化MMKV.initialize(this);

当然你也可以这样写。用于查看你的缓存文件存在哪里

 String initialize = MMKV.initialize(this);System.out.println("MMKV INIT " + initialize);

2. 数据存取

下面我会写一个工具类用来处理缓存数据的存取,在com.llw.mvvm包下新增一个utils包,包下新建一个MVUtils类,里面的代码如下:

public class MVUtils {private static MVUtils mInstance;private static MMKV mmkv;public MVUtils() {mmkv = MMKV.defaultMMKV();}public static MVUtils getInstance() {if (mInstance == null) {synchronized (MVUtils.class) {if (mInstance == null) {mInstance = new MVUtils();}}}return mInstance;}/*** 写入基本数据类型缓存** @param key    键* @param object 值*/public static void put(String key, Object object) {if (object instanceof String) {mmkv.encode(key, (String) object);} else if (object instanceof Integer) {mmkv.encode(key, (Integer) object);} else if (object instanceof Boolean) {mmkv.encode(key, (Boolean) object);} else if (object instanceof Float) {mmkv.encode(key, (Float) object);} else if (object instanceof Long) {mmkv.encode(key, (Long) object);} else if (object instanceof Double) {mmkv.encode(key, (Double) object);} else if (object instanceof byte[]) {mmkv.encode(key, (byte[]) object);} else {mmkv.encode(key, object.toString());}}public static void putSet(String key, Set<String> sets) {mmkv.encode(key, sets);}public static void putParcelable(String key, Parcelable obj) {mmkv.encode(key, obj);}public static Integer getInt(String key) {return mmkv.decodeInt(key, 0);}public static Integer getInt(String key, int defaultValue) {return mmkv.decodeInt(key, defaultValue);}public static Double getDouble(String key) {return mmkv.decodeDouble(key, 0.00);}public static Double getDouble(String key, double defaultValue) {return mmkv.decodeDouble(key, defaultValue);}public static Long getLong(String key) {return mmkv.decodeLong(key, 0L);}public static Long getLong(String key, long defaultValue) {return mmkv.decodeLong(key, defaultValue);}public static Boolean getBoolean(String key) {return mmkv.decodeBool(key, false);}public static Boolean getBoolean(String key, boolean defaultValue) {return mmkv.decodeBool(key, defaultValue);}public static Float getFloat(String key) {return mmkv.decodeFloat(key, 0F);}public static Float getFloat(String key, float defaultValue) {return mmkv.decodeFloat(key, defaultValue);}public static byte[] getBytes(String key) {return mmkv.decodeBytes(key);}public static byte[] getBytes(String key, byte[] defaultValue) {return mmkv.decodeBytes(key, defaultValue);}public static String getString(String key) {return mmkv.decodeString(key, "");}public static String getString(String key, String defaultValue) {return mmkv.decodeString(key, defaultValue);}public static Set<String> getStringSet(String key) {return mmkv.decodeStringSet(key, Collections.<String>emptySet());}public static Parcelable getParcelable(String key) {return mmkv.decodeParcelable(key, null);}/*** 移除某个key对** @param key*/public static void removeKey(String key) {mmkv.removeValueForKey(key);}/*** 清除所有key*/public static void clearAll() {mmkv.clearAll();}
}

这里的代码很简单,就是数据的存和取,下面我们来使用它,就在LoginActivity中做一个测试吧,在测试之前还需要在Application中对这个MVUtils类进行一个初始化。

 //工具类初始化MVUtils.getInstance();

截图如下:

3. 使用

  在LoginActivity中的onCreate方法中写入如下代码:

 //存Log.d("TAG", "onCreate: 存");MVUtils.put("age",24);//取int age = MVUtils.getInt("age",0);Log.d("TAG", "onCreate: 取 :" + age);

很简单的代码就是存一个int类型的值,然后取一个int类的值。

下面运行一下,只要进入到LoginActivity即可:

是不是可以呢?可以的话就进行下一步了,Room的使用了。记得把测试的代码给删掉啊。

三、Room

  Room 在开发阶段通过注解的方式标记相关功能,编译时自动生成响应的 impl 实现类。而下面关于创建数据库、创建表、创建Dao类,都与注解有关系。

1. @Entity

  下面我们来进行创建,在此之前我现在com.llw.mvvm包下新建一个db包。db包下新建一个AppDatabase类,空类就好。然后在db包下新建一个bean包,bean包下新建一个Image类,我们可以分析一下需要存到数据库中的值,是否所有数据都要存入,不要做没必要的事情,那是给自己找事。

从网络返回的数据可以得知,我使用的是其实就只有一小部分,那么我把这一小部分抽离出来做一个bean,Image类的代码如下:

@Entity
public class Image {@PrimaryKeyprivate int uid;private String url;private String urlbase;private String copyright;private String copyrightlink;private String title;public int getUid() {return uid;}public void setUid(int uid) {this.uid = uid;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getUrlbase() {return urlbase;}public void setUrlbase(String urlbase) {this.urlbase = urlbase;}public String getCopyright() {return copyright;}public void setCopyright(String copyright) {this.copyright = copyright;}public String getCopyrightlink() {return copyrightlink;}public void setCopyrightlink(String copyrightlink) {this.copyrightlink = copyrightlink;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public Image(){}@Ignorepublic Image(int uid, String url, String urlbase, String copyright, String copyrightlink, String title) {this.uid = uid;this.url = url;this.urlbase = urlbase;this.copyright = copyright;this.copyrightlink = copyrightlink;this.title = title;}
}

  这里用到了@Entity和@PrimaryKey一个表示数据库中的表名,一个是主键名,这里你也可以设置主键自增,我这里不设置是因为我永远只有一条数据,因此就没有必要。而这里还有一个构造方法,为了写数据方便一些,这个方法我们并不需要写入到数据库中,因此一旦我们写了一个有参数的构造方法则需要通过@Ignore将这个构造方法忽略掉,同时也要增加一个无参的构造方法,当然了@Ignore也可以用在别的参数上,除了主键,其他的无用变量都可以加@Ignore,加了就不会在表中出现。

下面来看看操作数据的类

2. @Dao

  在db包下新建一个dao包,dao包下新建一个ImageDao接口,里面的代码如下:

@Dao
public interface ImageDao {@Query("SELECT * FROM image")List<Image> getAll();@Query("SELECT * FROM image WHERE uid LIKE :uid LIMIT 1")Image queryById(int uid);@Insert(onConflict = OnConflictStrategy.REPLACE)void insertAll(Image... images);@Deletevoid delete(Image image);
}

现在可以更改这个AppDatabase类了。

3. @Database

  这里会用到第三个注解,修改后的AppDatabase代码如下:

@Database(entities = {Image.class},version = 1,exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {public abstract ImageDao imageDao();
}

这里通过注解的方式创建了一个数据库,同时在里面建了一个表,设置了当前的数据库版本,并且不允许导出。里面定了一个抽象方法imageDao()。Room库会采用编译时技术对这个ImageDao 进行实现。

4. 初始化

  Room数据库的初始化依然要放在BaseApplication当中,增加一个变量。

 //数据库public static AppDatabase db;

然后在onCreate中进行数据库的创建,代码如下:

 //创建本地数据库db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, "mvvm_demo").build();

这里创建了名为mvvm_demo的本地数据库。

然后再在BaseApplication中增加一个getDb方法。

 public static AppDatabase getDb(){return db;}

5. 使用

  在上一篇文章中,我将数据请求的代码放在MainRepository中,而使用Room数据库的代码也是在这个MainRepository里面,这里面的代码会做改动,而且改动很大。首先说一下改动思路吧,首先必应每日的壁纸是一样的,因此无论你是请求一次还是多次得到的值都是一样的,因此可以通过一个缓存再来确定设置今天是否有请求过网络接口,有的话再根据一个缓存值判断当前时间是否超过了今天的24点。没有超过就去本地数据库数据,超过了就请求网络。那么这个编码思路就很明确了。

先增加一个常量类,在utils包下新建一个Constant类,里面的代码如下:

public class Constant {/*** 今日请求接口返回数据的时间戳*/public static final String REQUEST_TIMESTAMP = "requestTimestamp";/*** 今日是否请求了接口*/public static final String IS_TODAY_REQUEST = "isTodayRequest";
}

首先是保存到本地数据库的方法,我们将在网络请求之后调用这个方法,代码如下:

 private static final String TAG = MainRepository.class.getSimpleName();final MutableLiveData<BiYingResponse> biyingImage = new MutableLiveData<>();/*** 保存数据*/private void saveImageData(BiYingResponse biYingImgResponse) {//记录今日已请求MVUtils.put(Constant.IS_TODAY_REQUEST,true);//记录此次请求的时最晚有效时间戳MVUtils.put(Constant.REQUEST_TIMESTAMP,DateUtil.getMillisNextEarlyMorning());BiYingResponse.ImagesBean bean = biYingImgResponse.getImages().get(0);//保存到数据库new Thread(() -> BaseApplication.getDb().imageDao().insertAll(new Image(1,bean.getUrl(),bean.getUrlbase(),bean.getCopyright(),bean.getCopyrightlink(), bean.getTitle()))).start();}

然后我们新增一个网络请求的方法。

 /*** 从网络上请求数据*/@SuppressLint("CheckResult")private void requestNetworkApi() {Log.d(TAG, "requestNetworkApi: 从网络获取");ApiService apiService = NetworkApi.createService(ApiService.class);apiService.biying().compose(NetworkApi.applySchedulers(new BaseObserver<BiYingResponse>() {@Overridepublic void onSuccess(BiYingResponse biYingImgResponse) {//存储到本地数据库中,并记录今日已请求了数据saveImageData(biYingImgResponse);biyingImage.setValue(biYingImgResponse);}@Overridepublic void onFailure(Throwable e) {KLog.e("BiYing Error: " + e.toString());}}));}

最后是一个从本地获取数据的方法

 /*** 从本地数据库获取*/private void getLocalDB() {Log.d(TAG, "getLocalDB: 从本地数据库获取");BiYingResponse biYingImgResponse = new BiYingResponse();new Thread(() -> {//从数据库获取Image image = BaseApplication.getDb().imageDao().queryById(1);BiYingResponse.ImagesBean imagesBean = new BiYingResponse.ImagesBean();imagesBean.setUrl(image.getUrl());imagesBean.setUrlbase(image.getUrlbase());imagesBean.setCopyright(image.getCopyright());imagesBean.setCopyrightlink(image.getCopyrightlink());imagesBean.setTitle(image.getTitle());List<BiYingResponse.ImagesBean> imagesBeanList = new ArrayList<>();imagesBeanList.add(imagesBean);biYingImgResponse.setImages(imagesBeanList);biyingImage.postValue(biYingImgResponse);}).start();}

最后修改getBiYing方法中的代码,修改后代码如下:

    public MutableLiveData<BiYingResponse> getBiYing() {//今日此接口是否已请求if (MVUtils.getBoolean(Constant.IS_TODAY_REQUEST)) {if(DateUtil.getTimestamp() <= MVUtils.getLong(Constant.REQUEST_TIMESTAMP)){//当前时间未超过次日0点,从本地获取getLocalDB();} else {//大于则数据需要更新,从网络获取requestNetworkApi();}} else {//没有请求过接口 或 当前时间,从网络获取requestNetworkApi();}return biyingImage;}

  这里当使用Room数据库时默认是不能在主线程中使用的,因此我这里新开一个子线程去处理,当然其实有更优雅的办法,后面我们再说,先看看这样写行不行。


  这里你会发现第一次进入的时候有一些延迟图片才加载出来,第二次进入的时候就感觉不到延迟了,因为从本地取数据比在网络要快很多,这是属于一种性能上的优化了,加载速度优化。下面我们再看看日志,看第一次是不是从网络请求,第二次是不是从本地数据库获取数据。

嗯,达到了预期,不过这里的逻辑还有一个问题,看有没有读者发现,发现了怎么去解决。很多能力要进步其实都在实践中,不要注重嘴上的知识,而要注重心里的知识。

6. 优化

  之前的写法还存在一定的问题,等我改完你就知道是什么问题了。修改AppDatabase中的代码,新增代码如下:

 private static final String DATABASE_NAME = "mvvm_demo";private static volatile AppDatabase mInstance;/*** 单例模式*/public static AppDatabase getInstance(Context context) {if (mInstance == null) {synchronized (AppDatabase.class) {if (mInstance == null) {mInstance = Room.databaseBuilder(context.getApplicationContext(),AppDatabase.class, "mvvm_demo").build();}}}return mInstance;}

然后修改BaseApplication中的代码。

然后你再运行一下,你会发现没啥变化,但是代码质量就上去了。

四、RxJava2

  Room数据库的使用是可以支持RxJava2、RxJava3的,这里我们使用RxJava2,在前面添加依赖的时候就已经添加进去了,因为要很好的解决Room的对数据处理的方式归根究底还是要做线程处理,我之前的那种方式虽然可以完成任务,但是并不推荐这样写,显示的调用不太好,你可通过创建线程池去做处理,当然了有更好的框架为什么不去用呢。因此就是用RxJava2了,你可能会疑惑之前不是在搭建网络框架的时候就用了RxJava2的线程切换了吗?为什么现在还要重新引入一个库来写呢?因为RxJava2是ReactiveX的开源库,虽然具备基本功能,但是不可能回去根据Google的JetPack的组件改动而改动,如果Google就需要自己去做一个适配,那就是让它的Room去支持RxJava2、RxJava3,这样是一种双赢。

1. Flowable&Completable

  好了,下面正式使用吧。首先我们去修改ImageDao中的代码,如下图所示:

  这里我增加了一个Flowable和Completable。由于读取速率可能 远大于 观察者处理速率,故使用背压 Flowable 模式,这是为了防止表中数据过多,读取速率远大于接收数据,从而导致内存溢出的问题,Completable就是操作完成的回调,可以感知操作成功或失败, onComplete和onError。

2. CustomDisposable

  针对于两种默认可以写一个自定义工具类,用于处理两种不同的结果处理。在repository包下新增一个CustomDisposable,里面的代码如下:

public class CustomDisposable {private static final CompositeDisposable compositeDisposable = new CompositeDisposable();/*** Flowable* @param flowable* @param consumer* @param <T>*/public static <T> void addDisposable(Flowable<T> flowable, Consumer<T> consumer) {compositeDisposable.add(flowable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(consumer));}/*** Completable* @param completable* @param action* @param <T>*/public static <T> void addDisposable(Completable completable, Action action) {compositeDisposable.add(completable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(action));}
}

这里就是通过compositeDisposable去安排线程的切换,这里需要先去了解一下RxJava的使用,不然你可能会云里雾里,下面回到MainRepository中。

3. 使用

  修改MainRepository中的saveImageData方法的代码。

修改getLocalDB方法代码。

运行一下,看看日志:

  本文到这里就结束了,希望能对你有所帮助。山高水长,后会有期~

五、源码

GitHub:MVVM-Demo 欢迎Star和Fork
CSDN:MVVMDemo_3.rar

Android MVVM框架搭建(三)MMKV + Room + RxJava2相关推荐

  1. Android MVVM框架搭建(十)Hilt、ViewBinding、Activity Result API

    Android MVVM框架搭建(十)Hilt.ViewBinding.Activity Result API 前言 正文 一.依赖 二.Hilt使用 1. Hilt 应用类 2. ViewModel ...

  2. Android MVVM框架搭建(二)OKHttp + Retrofit + RxJava

    Android MVVM框架搭建(二)Retrofit + RxJava 前言 正文 一.引入依赖 二.工具类 三.构建网络框架 1. Base 2. 异常处理 3. 拦截器 4. 网络请求服务 四. ...

  3. Android MVVM框架搭建(八)高德地图定位、天气查询、BottomSheetDialog

    Android MVVM框架搭建(八)高德地图定位.天气查询.BottomSheetDialog 前言 正文 一.集成SDK 二.基础配置 ① 权限配置 ② 配置Key 三.显示地图 ① MapFra ...

  4. Android MVVM框架搭建(七)Permission、AlertDialog、拍照和相册选取

    Android MVVM框架搭建(七)Permission.AlertDialog.拍照和相册选取 前言 正文 一.数据库升级 二.数据操作 二.自定义Dialog ① DialogViewHelpe ...

  5. Android MVVM框架搭建(九)TabLayout、ViewPager、城市地图天气切换

    Android MVVM框架搭建(九)TabLayout.ViewPager.城市地图切换 前言 正文 一.父Fragment加载子Fragment ① Fragment适配器 ② TabLayout ...

  6. android mvvm框架搭建_轻松搭建基于JetPack组件的MVVM框架

    原文链接:轻松搭建基于JetPack组件的MVVM框架 - 掘金 Brick github gitee 介绍 辅助android开发者搭建基于JetPack组件构建MVVM框架的注解处理框架.通过注解 ...

  7. Android MVVM封装,MVVM: 这是一个android MVVM 框架,基于谷歌dataBinding技术实现

    MVVM 这是一个android MVVM 框架,基于谷歌dataBinding技术实现.dataBinding 实现的 V 和 VM的关联:使用IOC架构实现了 M 和 V的关联. 框架具有以下功能 ...

  8. android mvvm官方文档,MVVM: 这是一个android MVVM 框架,基于谷歌dataBinding技术实现

    MVVM 这是一个android MVVM 框架,基于谷歌dataBinding技术实现.dataBinding 实现的 V 和 VM的关联:使用IOC架构实现了 M 和 V的关联. 框架具有以下功能 ...

  9. android+捕获google账户+cancel按钮,MVVM: 这是一个android MVVM 框架,基于谷歌dataBinding技术实现...

    MVVM 这是一个android MVVM 框架,基于谷歌dataBinding技术实现.dataBinding 实现的 V 和 VM的关联:使用IOC架构实现了 M 和 V的关联. 框架具有以下功能 ...

最新文章

  1. 迷茫的未来,我们需要做些什么呢?
  2. altera fpga 型号说明_基于FPGA的USB2.0接口通信
  3. C# 结合 Golang 开发
  4. python发布_python网站发布
  5. 简易付微信收款提示支付失败
  6. 软件测试基础课程学习笔记3---软件测试方法
  7. java中对象与字节数组相互转换
  8. 线性回归 - LinearRegression - 预测糖尿病 - 量化预测的质量
  9. Winform 五种常用对话框控件的简单使用
  10. Guice依赖注入(Provider)
  11. 【单片机】4.2 AT89S52中断系统结构
  12. PDP context激活的大致原理
  13. SpringBoot整合JDBC的Druid数据源
  14. 计算机磁盘清理软件,Cleanmgr+(电脑磁盘清理软件)
  15. 软件工程与软件开发模型、软件开发方法
  16. “人各有志,给分六十”
  17. 关于ORACLE索引
  18. 更开放的苹果能否改变iOS越狱市场的未来?
  19. vs2017 pdo mysql_在VS2017上使用Objectarx 2019向导
  20. 微信小程序 java springboot理发店美容店预约系统

热门文章

  1. 微信文件删除了怎么恢复,2个实测有效的办法推荐
  2. 玩转华为数据中心交换机系列 | 配置M-LAG双归接入普通以太网络示例
  3. xinetd 服务
  4. rand()函数和 srand()函数的用法
  5. java 里面耦合和解耦
  6. js中push使用 (数组)
  7. dw8php如何使用,如何用DW8实现网页区域内选择显示
  8. linux shell 中文输入,Linux操作系统的Shell环境中输入中文
  9. 关于STM32利用TIM+PWM+DMA控制WS2812
  10. 不讲武德!小伙陪大爷下棋靠手机开挂艰难获胜:我选的是天人合一难度