当前位置: 代码迷 >> 综合 >> Android MVVM框架搭建(三)MMKV + Room + RxJava2
  详细解决方案

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

热度:11   发布时间:2023-12-18 15:53:45.0

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

  相关解决方案