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