When it comes to actually touching and using the MVVM architecture, the whole person is no good. Because personally, COMPARED with MVC and MVP, MVVM is more difficult to learn, and the knowledge of design is not the slightest bit. So I want to slowly record my growth. Please correct any mistakes.
Building an MVVM Architecture from Scratch series of articles (continuing updates) : Android builds MVVM from scratch (1) ————DataBinding Android builds MVVM from scratch (2) ————ViewModel Android builds MVVM from scratch (3) ————LiveData Android builds MVVM architecture from scratch (4) ————Room (from beginner to advanced) Android builds MVVM architecture from scratch (5) ————Lifecycles Android builds MVVM architecture from scratch (6) ———— Uses Android to play ———— Use the Android API to take you to build the MVVM framework (final)
AAC(Android Architecture Components)
In this article, we will talk about Room, so that we can finally apply it to our MVVM project after understanding and understanding Room. This article is my own summary, if there are mistakes, please correct
I. Introduction and brief understanding of Room
Room is a library of SQLite abstraction layers that Google provides to simplify old SQLite operations
What it does: Implements SQLite’s add, delete, change and query (add, delete, change and query via annotations, similar to Retrofit).
When using Room, there are 4 modules:
- Bean: Entity class that represents data for a database table
- Dao: Data manipulation class that contains methods for accessing a database
- Database: Database owner & Database version manager
- Room: The creator of the database & the concrete implementer responsible for updating the database version
The difference from Greendao (which is only superficial here) is that the database is also based on ORM schema encapsulation. Compared with other ORMs, Room has the advantages of verifying the normality of query statements at compile time and supporting the return of LiveData data. We chose Room more because of the perfect support for LiveData. It also supports RxJava, and we all know that database operations, which are time-consuming operations, should be kept in child threads, so it works perfectly with RxJava and LiveData. Because they’re all asynchronous
// Add Room dependencies
implementation 'android. Arch. Persistence. Room: the runtime: 2.1.4'
annotationProcessor 'android. Arch. Persistence. Room: the compiler: 2.1.4'
Copy the code
Beans: entity classes that represent data in a database table
This means that we need to build tables and fields into the database. Use this bean object. First, a note
- @entity: The Entity class of the data table.
- @primaryKey: Every entity class needs a unique identity.
- @columnInfo: Name of a column in a data table.
- @ignore: Annotate attributes that do not need to be added to the data table.
- @Embedded: An entity class references another entity class.
- @foreignkey: ForeignKey constraint.
Here we create a Person class (to keep the data persistent and Room must be able to manipulate it, you can decorate the property with public, or you can set it to private, but you must provide set and get methods). Here is just a simple demonstration, and I will explain it in detail later. I think there are too many details
Alter table person;
@Entity
public class Person {
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "uid")
private int uid;
private String name;
private int age;
@Ignore
private int money;
@Embedded
private Address address;
/ /... I'm using private, leaving out the set and get methods for now. Easy for readers to understand
}
Copy the code
Address class:
public class Address {
private String city;
private String street;
/ /... Part of the code is omitted for easy understanding
}
Copy the code
2.1, @ the Entity
The class with the @Entity annotation represents the class name of the current class as the table name, and all properties in the class as the fields in the table. We’ll focus on @Entity for now, but we’ll go into a lot of detail later on. More to the point
2.1.1. If we do not want to use the class name as the table name, we can do so
// In this case, our table name becomes other
@Entity(tableName = "other")
public class Person {}Copy the code
2.1.2 compound primary key in @entity
In Person, we identified the uid PrimaryKey with @primarykey (autoGenerate = true) and set it to auto-grow. Fields set to the primary key must not be empty or have duplicate values.
Compound primary key: A multi-field primary key cannot duplicate the combination of multiple fields that make up the primary key (if we use name as the primary key, if we insert two data with the same name together, the data will be overwritten. In this case, we use name and date of birth as the compound primary key. In this case, the data will be overwritten only when the primary key is the same.
@Entity(primaryKeys = {"uid"."name"})
public class Person {}Copy the code
Run the project directly after setting it up like this. Here are a few things to note:
- You must annotate primary keys with @nonnull. “name” is nullable. So plus,
@Entity(primaryKeys = {"uid"."name"})
public class Person {
// The name field is annotated with @nonnull
@NonNull
private String name;
}
Copy the code
2.1.3. Use of indexes in @entity
Use of indexes (single column indexes, combined indexes, and index uniqueness)
Indices = {@index (value = "name")}
Indices = {@index (value = "name", unique = true)})
/ / composite Index @ Entity (indices = {@ Index (value = {" name ", "age"})})
Indices ={@index (value ={"name","age"},unique = true)})
// You can mix it up as follows:
@Entity(indices ={@Index(value = "name"),@Index(value = {"name"."age"},unique = true)})
public class Person {}Copy the code
- Database index is used to improve the speed of database access, can be said to be pure optimization meaning. After we add the index, nothing else changes
- Duplicate data is an error if uniqueness is added, somewhat like a primary key, but indexes are not conditional on overwriting data like primary keys
- When inserting data and action @ Insert (onConflict = OnConflictStrategy. REPLACE) plus action, he means the same primary key, the old data will REPLACE the new data. But if we have a different primary key, but we add index uniqueness, if we have the same index, then the insert fails. I’m sure that makes sense
2.1.4 Foreign key constraints in @entity
Also using the previous Person as the parent class, let’s make a Clothes class. (The Dao, Database, and Room steps are omitted and will be covered later)
Clothes:
@Entity(foreignKeys = @ForeignKey(entity = Person.class,parentColumns = "uid",childColumns = "father_id"))
public class clothes {
@PrimaryKey(autoGenerate = true)
private int id;
private String color;
private int father_id;
/ /... Omit get and set
}
Copy the code
A foreign key constraint is a foreign key constraint, so let’s insert some data in db:
1, (uid =1 name = magma age= 18) 2, (uid =2 name = age=10);
The second part: (id = 1 color = red father_id = 1) (id = 2 color = black father_id = 1) (id = 3 color = red father_id = 1) (id = 3 color = red father_id = 1 2)
The person Magma has two clothes, red and black. The pupil has one dress, the red one. Let’s see what the watch looks like. ParentColumns = “uid” (the uid field of person) as childColumns = “father_id” (the father_id field of clothes). So this is kind of a constraint. Not so fast. Let’s look at two tables.
The person table (there will be a tutorial later on how to look at the DB database) :
Thanks table
So why is it a foreign key constraint? Of course there are operations. As follows:
@Entity(foreignKeys = @ForeignKey(onDelete = CASCADE,onUpdate = CASCADE,entity = Person.class,parentColumns = "uid",childColumns = "father_id"))
public class clothes {}Copy the code
Here I added two actions, onDelete = CASCADE and onUpdate = CASCADE for delete and update. Here are the following actions:
- NO_ACTION: The father_id does nothing when the uid in person changes
- RESTRICT: Disables actions on person when the UID in person is dependent on a clothes object, which results in an error.
- SET_NULL: The father_id of the clothes is set to NULL when the uid in person changes.
- Set_default: The father_id of the clothes is set to the DEFAULT value when the uid in person changes. In this case, the father_id is set to 0
- CASCADE: When the uid of person changes, the father_id of the clothes table will change. If I delete the uid of person, the father_id of the clothes table will be deleted.
Is it clear now? A lot of blogs do. I tried to make it clear. Give the blogger a thumbs up. Article demo does not do processing, when observing, remember to observe in order.
2.2, @ PrimaryKey
// Omit some code for easy understanding
public class Person {
//person does not need to match the primary key, we can simply default uid as the primary key
@primaryKey (autoGenerate = true)
@PrimaryKey
private int uid;
}
Copy the code
2.3, @ ColumnInfo
We all know that the attribute value name in Person is the name of the field in the table. You can do this if you don’t want to use the property name as the field name
// Omit some code for easy understanding
public class Person {
// My primary key in the table is uid_
@ColumnInfo(name = "uid_")
private int uid;
}
Copy the code
2.4, @ Ignore
If you don’t want the attribute value as a field in the table, ignore it
// Omit some code for easy understanding
public class Person {
// Let's ignore the money transfer, people need money why.
@Ignore
private int money;
}
Copy the code
2.5, @ Embedded
Entity classes refer to other entity classes. In this case, the Address property also becomes a field in the table Person.
// Omit some code for easy understanding
public class Person {
@Embedded
private Address address;
}
Copy the code
We have two fields in Address, city and street, so our table also has two fields
So if there’s a special place, let’s say this person is rich, has 2 homes, has 2 Address classes, what do we do?
// @embedded (prefix = "one"), this is unique, for example, a person may have two addresses similar to the tag, then the table will be named with the prefix+ attribute value
@Embedded(prefix = "one")
private Address address;
@Embedded(prefix = "two")
private Address address;
Copy the code
Dao: Data manipulation class, which contains methods for accessing a database
Here is the code directly, the relevant annotation is:
- @DAO: Class to annotate database operations.
- @query: contains all Sqlite statement operations.
- @INSERT: Annotates database Insert operations.
- @delete: indicates the database deletion operation.
- @update: Indicates the Update operation of the database.
@Dao
public interface PersonDao {
// Query all data
@Query("Select * from person")
List<Person> getAll(a);
// Delete all data
@Query("DELETE FROM person")
void deleteAll(a);
// Insert one or more data at a time
/ / @ Insert (onConflict = OnConflictStrategy. REPLACE), is this why? Here are the detailed tutorial
@Insert
void insert(Person... persons);
// Delete one or more data at a time
@Delete
void delete(Person... persons);
// Update one or more data items at a time
@Update
void update(Person... persons);
// Find data by field
@Query("SELECT * FROM person WHERE uid= :uid")
Person getPersonByUid(int uid);
// Find more data at once
@Query("SELECT * FROM person WHERE uid IN (:userIds)")
List<Person> loadAllByIds(List<Integer> userIds);
// Multiple criteria search
@Query("SELECT * FROM person WHERE name = :name AND age = :age")
Person getPersonByNameage(String name, int age);
}
Copy the code
The only thing special here is at sign Insert. There is an introduction: when designing a database, duplicate data is not allowed. Otherwise, a large number of redundant data is bound to result. In fact, it’s hard to avoid this problem: conflict. When we insert data into a database, the data already exists, causing a collision. How should the conflict be handled? Conflict is used in the @insert annotation to resolve Insert data conflicts. Its default value is onconflictStrategy.abort. For OnConflictStrategy, it encapsulates Room’s conflict-resolution policies.
- OnConflictStrategy. REPLACE: conflict strategy is to REPLACE the old data to continue the transaction at the same time
- OnConflictStrategy. ROLLBACK: conflict strategy is to roll back the transaction
- Onconflictstrategy. ABORT: The conflict policy is to terminate the transaction
- Onconflictstrategy. FAIL: The conflict policy is a transaction failure
- OnConflictStrategy. IGNORE: conflict strategy is to IGNORE the conflict
Here such as the insert we added OnConflictStrategy. REPLACE, then to have the uid = 1 person list to insert the uid = 1 person data, then the new data will overwrite the data. If we do nothing, onConflictStrategy. ABORT is the default. This is the termination transaction mentioned above. Others you can try yourself
Database: Database holder & Database version manager
Go straight to code
// The annotation specifies the database's table mapping entity data and version information (more on version updates later)
@Database(entities = {Person.class, Clothes.class}, version = 1)
public abstract class AppDataBase extends RoomDatabase {
public abstract PersonDao getPersonDao(a);
public abstract ClothesDao getClothesDao(a);
}
Copy the code
5. Room: the creator of the database & the implementer responsible for updating the database version
Room creates our AppDataBase, and we encapsulate it as a singleton so that we don’t have to execute it every time and consume performance
public class DBInstance {
//private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
private static final String DB_NAME = "room_test";
public static AppDataBase appDataBase;
public static AppDataBase getInstance(a){
if(appDataBase==null) {synchronized (DBInstance.class){
if(appDataBase==null){
appDataBase = Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
The following comment indicates that the main thread is allowed to perform database operations, but is not recommended.
// I am here for Demo, I will finish working with LiveData and RxJava later.allowMainThreadQueries() .build(); }}}returnappDataBase; }}Copy the code
Having done all this, our preparation will be done. Let’s insert a piece of data
Person person_ = new Person("Room".18);
DBInstance.getInstance().getPersonDao().insert(person_);
Copy the code
5.1. Additional knowledge
How to view the DB data here? First we save the DB file in the phone memory, remember to open the storage permission, is in the above code to specify the path
private static final String DB_NAME = “/sdcard/LianSou/room_test.db”; After the data is inserted, a DB file is generated on the phone’s memory card.
Get the DB file, how to do. Use plugins!! Database Navigator, plugin tutorial
Vi. Database version upgrade
I mean, I’ve already stored data into the person table. But I’m going to add fields, or I’m going to add indexes. If you write it directly, you will find that when you use the database again, it will crash directly. So what do we do, as anyone who’s used Greendao knows, is we need to update the database
@Entity
public class Person {
/ /... Part of the code is omitted for easy understanding.
// Add a son to the Person
}
Copy the code
Then go to our Database class, change the version information, and add a Migration class to tell Room which table changed what
// Change the version to 2
@Database(entities = {Person.class, Clothes.class}, version = 2)
public abstract class AppDataBase extends RoomDatabase {
public abstract PersonDao getPersonDao(a);
public abstract ClothesDao getClothesDao(a);
// Database changes add Migration, which is short for version 1 to version 2
public static final Migration MIGRATION_1_2 = new Migration(1.2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Tell the person table to add a String field son
database.execSQL("ALTER TABLE person ADD COLUMN son TEXT"); }}; }Copy the code
For usage in version update execSQL, see Room Upgrade. You can also baidu, a lot of online
And finally in our Room:
public class DBInstance {
// private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
private static final String DB_NAME = "room_test";
public static AppDataBase appDataBase;
public static AppDataBase getInstance(a){
if(appDataBase==null) {synchronized (DBInstance.class){
if(appDataBase==null) {return Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
.allowMainThreadQueries()
// Add version update information.addMigrations(AppDataBase.MIGRATION_1_2) .build(); }}}returnappDataBase; }}Copy the code
With that done, let’s run the project. Success, open the data to see (in this article demo, I put the upgrade code annotation, want to test can be opened) :
Vii. Use Room with RxJava (need to understand the use of RxJava first)
Let’s start with Room in DBInstance and create our AppDataBase
The following comment indicates that the main thread is allowed to perform database operations, but is not recommended.
.allowMainThreadQueries()
Copy the code
Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
At this point, we’ll use it in conjunction with RxJava, so that data operations can be placed in child threads and callbacks can be switched to the main thread to change the UI. The first is to introduce our dependencies
implementation 'android. Arch. Persistence. Room: rxjava2:2.1.4'
// This is used with RXJava
implementation 'the IO. Reactivex. Rxjava2: rxandroid: 2.0.2'
Copy the code
Two things to note here:
RxJava can be used for @insert, @delete, @update operations. The types in RxJava are Completable, Single, Maybe
2. When executing @query, you can return the following types: Single, Maybe, Observable, Flowable
- If you want to query once, use: Single, Maybe; This way, if the database changes after the query, there will be no subsequent transactions.
- If you want to look at databases: Observable, Flowable. The code observed in Observable and Flowable is automatically executed if the data changes after the query has been performed. This means that the data is continuously observed, showing the latest data in the database in real time
Here you may not know much about Single, Maybe, Completable, Observable and Flowable. Here is a brief introduction:
Completable: There are only onComplete and onError methods, i.e., only “complete” and “error” states, and no concrete results are returned.
2, Single: its callback is onSuccess and onError, the query success will return the result in onSuccess, it should be noted that if the query result is not found, that is, the query result is empty, the onError callback will directly run, throw EmptyResultSetException exception.
3, Maybe: its callback is onSuccess, onError, onComplete, query success, if there is data, will call onSuccess and then call onComplete, if there is no data, will directly call onComplete.
4. Flowable/Observable: Returns an Observable that calls back to onNext when the query changes, but not when there is no data change. Until the Rx stream is disconnected.
Error: Methods annotated with @insert can return either void, long, long, long[], long[] or List.
So now a lot of Internet about this part, also did not speak clearly. If there are clear students please correct. Look at the Dao class:
@Dao
public interface DogDao {
// The return value is the id of the successfully inserted row
@Insert
List<Long> insert(Dog... dogs);
@Delete
void delete(Dog... dogs);
// Returns the id of the deleted row
@Delete
int delete(Dog dog);
@Update
void update(Dog... dogs);
@Update
int update(Dog dog);
// Query all objects and observe the data. This can be done with back-pressure Flowable. If you need a one-time query, you can use another type
@Query("Select * from dog")
Flowable<List<Dog>> getAll();
// Delete all data
@Query("DELETE FROM dog")
void deleteAll(a);
// Find data by field
@Query("SELECT * FROM dog WHERE id= :id")
Single<Dog> getDogById(int id);
}
Copy the code
Let’s query all of our dogs in real time in code with observable back pressure. It only needs to be called once, and the observer callback is automatically invoked when the data is updated.
DBInstance.getInstance().getDogDao().getAll().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Dog>>() {
@Override
public void accept(List<Dog> dogs) throws Exception {
binding.txtAll.setText("Current dog count"+ dogs.size()); }});Copy the code
That’s what a lot of people ask. @insert, @delete, @update. Many blogs write the return value in the Dao. Real run up, direct error report. So you’re going to use RxJava in your code. Best encapsulated for use on projects. For example, insert data with Single :(which type is used depends on your needs. For example, after inserting data, I need to know the id of the inserted row. I can’t use Completable because it returns no value.)
Single.fromCallable(new Callable<List<Long>>() {
@Override
public List<Long> call(a) throws Exception {
Dog dog = new Dog();
return DBInstance.getInstance().getDogDao().insert(dog);
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<Long>>() {
@Override
public void onSubscribe(Disposable d) {}// Insert more data at once, return a set of row ids
@Override
public void onSuccess(List<Long> o) {
for (Long data : o) {
LogUtils.i("Insert data using Single"."onSuccess ==> "+ data); }}@Override
public void onError(Throwable e) {
LogUtils.i("Insert data using Single"."onError"); }});Copy the code
If you don’t need an observer callback, you can.
Single.fromCallable(new Callable<List<Long>>() {
@Override
public List<Long> call(a) throws Exception {
Dog dog = new Dog();
return DBInstance.getInstance().getDogDao().insert(dog);
}
}).subscribeOn(Schedulers.io())
.subscribe();
Copy the code
Mysql > update database; update database;
Vii. Room is used in combination with LiveData
Here we add the return value of LiveData to DogDao (query range ID for dog).
@Query("SELECT * FROM dog WHERE id>= :minId AND id<= :maxId")
LiveData<List<Dog>> getToLiveData(int minId, int maxId);
Copy the code
The code in the Activity:
DBInstance.getInstance().getDogDao().getToLiveData(2.12).observe(this.new Observer<List<Dog>>() {
@Override
public void onChanged(List<Dog> dogs) {
ToastUtils.showToast("Current value found ==>"+ dogs.size()); }});Copy the code
Remember we talked about LiveData earlier. At this time, LiveData follows the life cycle. OnChanged will only be called back if it is active and will cancel the observer if it is destroyed.
Some database instructions in Dao
@Query("SELECT * FROM TalkListBean WHERE father_id= :father_id ORDER BY isTop DESC limit 0,4")
Flowable<List<TalkListBean>> sortBydDesc(int father_id);
// Find the same father_id in the TalkListBean table (e.g. Chat list);
ORDER BY (isTop); ORDER BY (isTop);
// DESC is in reverse order, ASC is in forward order;
Limit 0,4:0 indicates the starting position of the query data, and 4 indicates the pageSize.
Copy the code
This is the end of the simple Room. I have to say that a lot of information online is very brainless, full of official translation. This article is the author’s own understanding, if there are mistakes please correct. Please give me a thumbs up when you see this
Demo address