Android MVVM framework construction (III) MMKV + Room + RxJava2
preface
in the last article, I described how to build a network access framework in the MVVM framework, and made an access demonstration of the request interface through a Bing daily wallpaper. This article needs to tell about the use of the local database on the Android side and how to use it in the MVVM.
text
this article is about the database. Why do we talk about this? Because in the actual development, some data does not need to be updated in real time. We only need to get it when we open the application for the first time, and then save it to the local database of the mobile phone, and get it from the database when necessary. When the data needs to be updated, it is obtained from the server, which can reduce the number of requests.
I'm talking about a component in JetPack, Room, which is a database component. In fact, it is also an upper encapsulation of Sqlite. Before Room, we will also use some third-party open source libraries, such as GreenDao, LitePal, ORMLite, etc. Of course, you can still use these open source libraries now. After all, you have formed the habit of using them. However, in line with the principle of technology without pressure, we can still understand, don't you think?
1, Add dependency
in the created project, there is no Room dependency by default, so it needs to be added manually. Add it under the dependencies {} closure in build.gradle of app, and the code is as follows:
//Room database implementation 'androidx.room:room-runtime:2.3.0' annotationProcessor 'androidx.room:room-compiler:2.3.0' //Room supports RxJava2 implementation 'androidx.room:room-rxjava2:2.3.0' //Tencent MMKV implementation 'com.tencent:mmkv:1.2.11'

Then click Sync Now to synchronize the project. After synchronization, you can start using it.
2, MMKV
SharedPreferences, which has been used in Android for many years, was finally abandoned by Google. A DataStore was added to the new component of JetPack. In fact, before the DataStore appeared, there were some third-party local cache processing libraries, such as Tencent's MMKV library. It is easy to use. I haven't used MMKV in my previous blog. Let's use it in this article, In fact, there is also a component in JetPack that is used to solve SharedPreferences, DataStore, but I found that its user group has not been up yet, so I won't introduce it first and changed it to MMKV library. Personally, this library is simpler to use than DataStore, which is also a very practical library.
in the above build.gradle configuration, I have added the latest dependency library. Let's use it. It's actually very simple.
1. Initialization
the first step is to initialize in the custom Application. Add the following code in the onCreate method:
//MMKV initialization MMKV.initialize(this);
Of course you can write that too. Used to see where your cache files exist
String initialize = MMKV.initialize(this); System.out.println("MMKV INIT " + initialize);
2. Data access
Next, I will write a tool class to handle the access of cached data. Add a utils package under com.llw.mvvm package and a MVUtils class under the package. The code is as follows:
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; } /** * Write basic data type cache * * @param key key * @param object value */ 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); } /** * Remove a key pair * * @param key */ public static void removeKey(String key) { mmkv.removeValueForKey(key); } /** * Clear all key s */ public static void clearAll() { mmkv.clearAll(); } }
The code here is very simple, that is, data storage and retrieval. Let's use it. Let's do a test in LoginActivity. Before the test, we also need to initialize this MVUtils class in the Application.
//Tool class initialization MVUtils.getInstance();
The screenshot is as follows:

3. Use
write the following code in the onCreate method in LoginActivity:
//Save Log.d("TAG", "onCreate: Save"); MVUtils.put("age",24); //take int age = MVUtils.getInt("age",0); Log.d("TAG", "onCreate: Take:" + age);
Very simple code is to save a value of type int, and then take a value of type int.
Run it below. Just enter LoginActivity:

Is it possible? If you can, go to the next step and use the Room. Remember to delete the test code.
3, Room
Room marks relevant functions in the development stage by annotation, and automatically generates the impl implementation class of response during compilation. The following about creating databases, tables and Dao classes are related to annotations.
1. @Entity
let's create a db package under the com.llw.mvvm package. Create a new AppDatabase class under db package, and just an empty class. Then create a bean package under the db package and an Image class under the bean package. We can analyze the values that need to be stored in the database and whether all data should be stored. Don't do unnecessary things. It's to find something for ourselves.

From the data returned from the network, it can be seen that I use only a small part. Then I extract this small part to make a bean. The code of Image class is as follows:
@Entity public class Image { @PrimaryKey private 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(){} @Ignore public 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; } }
here @ Entity and @ PrimaryKey are used. One represents the table name in the database, and the other is the primary key name. You can also set the primary key auto increment here. I don't set it here because I always have only one data, so it's not necessary. There is also a construction method here. For the convenience of writing data, we do not need to write this method into the database. Therefore, once we write a construction method with parameters, we need to Ignore this construction method through @ Ignore, and add a parameterless construction method. Of course, @ Ignore can also be used on other parameters, except the primary key, Other useless variables can be added @ Ignore, which will not appear in the table.
Let's take a look at the classes that manipulate data
2. @Dao
create a dao package under db package and an ImageDao interface under dao package. The code is as follows:
@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); @Delete void delete(Image image); }
You can now change the AppDatabase class.
3. @Database
the third annotation will be used here. The modified AppDatabase code is as follows:
@Database(entities = {Image.class},version = 1,exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract ImageDao imageDao(); }
Here, a database is created by annotation, a table is built in it, the current database version is set, and export is not allowed. It defines an abstract method imageDao(). The Room library will implement this ImageDao using compile time technology.
4. Initialization
the initialization of the Room database should still be placed in the BaseApplication, and a variable should be added.
//database public static AppDatabase db;
Then create the database in onCreate. The code is as follows:
//Create local database db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, "mvvm_demo").build();
Here you create a file called MVVM_ The local database of the demo.
Then add a getDb method to BaseApplication.
public static AppDatabase getDb(){ return db; }

5. Use
in the last article, I put the data request code in the main repository, and the code using the Room database is also in the main repository. The code in this main repository will be changed greatly. First, let's talk about the idea of changing. First, the wallpaper of Bing is the same every day. Therefore, whether you request it once or multiple times, the value obtained is the same. Therefore, you can determine whether the network interface has been requested today through a cache. If so, judge whether the current time exceeds 24 o'clock today according to a cache value. If not, go to the local database and request the network. Then the coding idea is very clear.
The first is the method of saving to the local database. We will call this method after the network request. The code is as follows:
private static final String TAG = MainRepository.class.getSimpleName(); final MutableLiveData<BiYingResponse> biyingImage = new MutableLiveData<>(); /** * Save data */ private void saveImageData(BiYingResponse biYingImgResponse) { //Record requested today MVUtils.put(Constant.IS_TODAY_REQUEST,true); //The latest valid timestamp when recording this request MVUtils.put(Constant.REQUEST_TIMESTAMP,DateUtil.getMillisNextEarlyMorning()); BiYingResponse.ImagesBean bean = biYingImgResponse.getImages().get(0); //Save to database new Thread(() -> BaseApplication.getDb().imageDao().insertAll( new Image(1,bean.getUrl(),bean.getUrlbase(),bean.getCopyright(), bean.getCopyrightlink(), bean.getTitle()))).start(); }
Then we add a new method of network request.
/** * Request data from the network */ @SuppressLint("CheckResult") private void requestNetworkApi() { Log.d(TAG, "requestNetworkApi: Get from network"); ApiService apiService = NetworkApi.createService(ApiService.class); apiService.biying().compose(NetworkApi.applySchedulers(new BaseObserver<BiYingResponse>() { @Override public void onSuccess(BiYingResponse biYingImgResponse) { //Store in the local database and record the data requested today saveImageData(biYingImgResponse); biyingImage.setValue(biYingImgResponse); } @Override public void onFailure(Throwable e) { KLog.e("BiYing Error: " + e.toString()); } })); }
Finally, there is a method to obtain data locally
/** * Get from local database */ private void getLocalDB() { Log.d(TAG, "getLocalDB: Get from local database"); BiYingResponse biYingImgResponse = new BiYingResponse(); new Thread(() -> { //Get from database 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(); }
Finally, modify the code in the getBiYing method. The modified code is as follows:
public MutableLiveData<BiYingResponse> getBiYing() { //Has this interface been requested today if (MVUtils.getBoolean(Constant.IS_TODAY_REQUEST)) { if(DateUtil.getTimestamp() <= MVUtils.getLong(Constant.REQUEST_TIMESTAMP)){ //The current time does not exceed 0 o'clock of the next day. Get it locally getLocalDB(); } else { //Greater than, the data needs to be updated and obtained from the network requestNetworkApi(); } } else { //No interface has been requested or the current time is obtained from the network requestNetworkApi(); } return biyingImage; }
when using the Room database, it cannot be used in the main thread by default. Therefore, I open a new sub thread to handle it. Of course, there are more elegant methods. We'll talk about it later. Let's see if this is OK.

here you will find that some delayed pictures are loaded when you enter for the first time, and you won't feel the delay when you enter for the second time, because it's much faster to get data from the local than on the network. This is a kind of performance optimization, load speed optimization. Next, let's look at the log to see whether the first time is to request from the network and whether the second time is to obtain data from the local database.

Well, it's up to expectations, but there's another problem in the logic here. See if readers find it and how to solve it. In fact, many abilities need to be improved in practice. Don't pay attention to the knowledge in your mouth, but pay attention to the knowledge in your heart.
6. Optimization
there are still some problems with the previous writing. You will know what the problem is when I finish changing it. Modify the code in AppDatabase and add the following code:
private static final String DATABASE_NAME = "mvvm_demo"; private static volatile AppDatabase mInstance; /** * Singleton mode */ 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; }
Then modify the code in the BaseApplication.

Then you run it again, and you will find that there is no change, but the code quality is up.
4, RxJava2
the use of Room database can support RxJava2 and RxJava3. Here we use RxJava2, which has been added when we added dependencies earlier, because threading is still needed to solve the data processing method of Room. Although my previous method can complete tasks, it is not recommended, The displayed call is not very good. You can create a thread pool to handle it. Of course, there is a better framework. Why not use it. Therefore, RxJava2 is used. You may wonder whether RxJava2 thread switching was used when building the network framework before? Why reintroduce a library to write now? Because RxJava2 is an open source library of ReactiveX. Although it has basic functions, it is impossible to change it according to the component changes of Google's JetPack. If Google needs to make an adaptation by itself, it is to let its Room support RxJava2 and RxJava3. This is a win-win situation.
1. Flowable&Completable
OK, let's officially use it. First, let's modify the code in ImageDao, as shown in the following figure:

here I add a Flowable and complete. Since the read rate may be much higher than the observer's processing rate, the back pressure Flowable mode is used to prevent excessive data in the table and the read rate is much higher than the received data, resulting in memory overflow. Completable is the callback for the completion of the operation, which can sense the success or failure of the operation, onComplete and onError.
2. CustomDisposable
for two default, you can write a user-defined tool class to handle two different result processing. Add a customdispose under the repository package. The code is as follows:
public class CustomDisposable { private static final CompositeDisposable compositeDisposable = new CompositeDisposable(); /** * Flowable * @param flowable * @param consumer * @param */ 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 */ public static <T> void addDisposable(Completable completable, Action action) { compositeDisposable.add(completable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(action)); } }
Here is to arrange thread switching through compositedispose. You need to learn about the use of RxJava first, otherwise you may be confused. Let's go back to the main repository.
3. Use
modify the code of the saveImageData method in the MainRepository.

Modify the getLocalDB method code.

Run it and look at the log:

this article ends here. I hope it can help you. I'll see you later~
5, Source code
GitHub: MVVM-Demo CSDN: MVVMDemo_3.rar