在真正接觸並使用MVVM架構的時候,整我的都很差了。由於我的以爲,MVVM相對於MVC、MVP學習難度比較大,設計的知識點不是一點半點。因此想慢慢記錄下本身的成長。若有錯誤但願指正。java
從零開始搭建MVVM架構系列文章(持續更新):
Android從零開始搭建MVVM架構(1)————DataBinding
Android從零開始搭建MVVM架構(2)————ViewModel
Android從零開始搭建MVVM架構(3)————LiveData
Android從零開始搭建MVVM架構(4)————Room(從入門到進階)
Android從零開始搭建MVVM架構(5)————Lifecycles
Android從零開始搭建MVVM架構(6)————使用玩Android API帶你搭建MVVM框架(初級篇)
Android從零開始搭建MVVM架構(7) ———— 使用玩Android API帶你搭建MVVM框架(終極篇)react
仍是那張圖AAC(Android Architecture Components)android
這篇咱們講Room,讓咱們瞭解和認識Room後,最終運用到咱們的MVVM的項目中去。本文是本身的總結,若有錯誤,請指正git
簡介:
Room是google爲了簡化舊式的SQLite操做專門提供的一個覆蓋SQLite抽象層框架庫github
做用:
實現SQLite的增刪改查(經過註解的方式實現增刪改查,相似Retrofit。)數據庫
在使用Room,有4個模塊:架構
與greendao的區別(這裏只是簡單從表面看):一樣基於ORM模式封裝的數據庫。而Room和其餘ORM對比,具備編譯時驗證查詢語句正常性,支持LiveData數據返回等優點。咱們選擇room,更可能是由於對LiveData的完美支持。同時也支持RxJava,咱們都知道數據庫操做這些耗時操做都應該放在子線程裏,因此配合RxJava和LiveData很完美了。由於他們都是異步的app
//添加Room的依賴
implementation 'android.arch.persistence.room:runtime:2.1.4'
annotationProcessor 'android.arch.persistence.room:compiler:2.1.4'
複製代碼
意思就是咱們要往數據庫裏建表、建字段。就是使用這個bean對象。首先介紹下註解框架
這裏咱們建一個Person類(爲了能保存數據,使數據持久化且Room必須可以對它進行操做,你能夠用public修飾屬性,或者你也能夠設置成private,但必須提供set和get方法)。這裏只是簡單展現,後面詳細講解,以爲細節太多了異步
表名爲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;
//...我用的是private,暫且去掉了set和get方法。便於讀者理解
}
複製代碼
public class Address {
private String city;
private String street;
//...省略部分代碼,便於理解
}
複製代碼
用了@Entity標註的類,表示當前類的類名做爲表名,這個類裏面的全部屬性,做爲表裏的字段。這裏咱們先只關注@Entity來說,後面又不少細節,文章接下來都以這種講解分格。更加直擊重點
//這樣的話,咱們的表名就變成了 other
@Entity(tableName = "other")
public class Person {
}
複製代碼
在Person裏,咱們用@PrimaryKey(autoGenerate = true)標識uid爲主鍵,且設置爲自增加。設置爲主鍵的字段不得爲空也不容許有重複值。
複合主鍵:多字段主鍵則構成主鍵的多個字段的組合不得有重複(假如咱們用name作主鍵,若是咱們有2個name相同的數據一塊兒插入,數據就會被覆蓋掉。可是現實中真的有同名的人,是2條數據,這時候咱們就要用name和出生日期做爲複合主鍵也就是多個主鍵,主鍵都一致纔會覆蓋數據)
@Entity(primaryKeys = {"uid","name"})
public class Person {
}
複製代碼
直接這樣設置後,運行項目。這裏有幾點要注意的:
@Entity(primaryKeys = {"uid","name"})
public class Person {
//name字段要用@NonNull標註
@NonNull
private String name;
}
複製代碼
索引的使用(有單列索引和組合索引,還有索引的惟一性)
//單列索引 @Entity(indices = {@Index(value = "name")})
//單列索引惟一性 @Entity(indices = {@Index(value = "name", unique = true)})
//組合索引 @Entity(indices ={@Index(value = {"name","age"})})
//組合索引惟一性 @Entity(indices ={@Index(value = {"name","age"},unique = true)})
//固然能夠混起來用 以下:
@Entity(indices ={@Index(value = "name"),@Index(value = {"name","age"},unique = true)})
public class Person {
}
複製代碼
一樣以以前的Person做爲父類,咱們再定一個衣服類Clothes。(這裏先省略Dao,Database,Room步驟,後面會細講)
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;
//...省略get和set
}
複製代碼
好多人不知道外鍵約束是什麼意思,這裏咱們先往裏面插數據,而後咱們看看db裏的數據:
第一步:咱們往Person裏面插入2填數據
一、(uid =1 name = 岩漿 age =18)
二、(uid =2 name = 小學生 age=10);
第二部:咱們往衣服裏面插入3條數據
一、(id = 1 color = 紅色 father_id = 1)
二、(id = 2 color = 黑色 father_id = 1)
三、(id = 3 color = 紅色 father_id = 2)
這裏其實顯而易見,能夠先認爲,person岩漿有2件衣服,紅色和黑色的衣服;person小學生有1件衣服,紅色的衣服。咱們看看錶是怎麼樣的。意思就是用parentColumns = "uid"(person的uid字段)做爲childColumns = "father_id"(clothes的father_id字段)。這裏就至關於約束到了。先不急,咱們看看2張表。
person表(後面會有教程,教你怎麼看db數據庫):
clothes表
那麼爲何說是外鍵約束呢?固然這裏有操做。以下:
@Entity(foreignKeys = @ForeignKey(onDelete = CASCADE,onUpdate = CASCADE,entity = Person.class,parentColumns = "uid",childColumns = "father_id"))
public class Clothes {
}
複製代碼
這裏我加了2個動做,在刪除和更新的時候用了onDelete = CASCADE,onUpdate = CASCADE。這裏動做有如下:
如今是否是很清楚了。不少博客都帶過。我也費力講清楚了。給博主個贊把。文章demo沒有作處理,在觀察時,記得請按順序觀察。
//省略部分代碼,便於理解
public class Person {
//person固然不須要符合主鍵,咱們能夠直接這樣默認uid爲主鍵
//想要自增加那麼這樣@PrimaryKey(autoGenerate = true)
@PrimaryKey
private int uid;
}
複製代碼
咱們都知道,Person裏的屬性值名就是表裏的字段名。假如不像用屬性名當字段名,能夠這樣
//省略部分代碼,便於理解
public class Person {
//那麼這個時候個人主鍵在表裏的key就是uid_
@ColumnInfo(name = "uid_")
private int uid;
}
複製代碼
若是不想要屬性值做爲表裏的字段,那麼忽略掉
//省略部分代碼,便於理解
public class Person {
//讓咱們忽略調錢,人要錢幹嗎。。
@Ignore
private int money;
}
複製代碼
實體類中引用其餘實體類。這樣的話Address裏屬性也成爲了表person的字段。
//省略部分代碼,便於理解
public class Person {
@Embedded
private Address address;
}
複製代碼
咱們Address裏有2個字段,city,street,因此咱們的表也是
這裏有個特殊的地方,好比說這我的頗有錢(剛剛纔忽略掉錢),有2個家,有2個Address類,那麼怎麼辦呢,
//@Embedded(prefix = "one"),這個是區分惟一性的,好比說一這我的可能有2個地址相似於tag,那麼在數據表中就會以prefix+屬性值命名
@Embedded(prefix = "one")
private Address address;
@Embedded(prefix = "two")
private Address address;
複製代碼
這裏直接上代碼,相關標註是:
@Dao
public interface PersonDao {
//查詢全部數據
@Query("Select * from person")
List<Person> getAll();
//刪除所有數據
@Query("DELETE FROM person")
void deleteAll();
//一次插入單條數據 或 多條
// @Insert(onConflict = OnConflictStrategy.REPLACE),這個是幹嗎的呢,下面有詳細教程
@Insert
void insert(Person... persons);
//一次刪除單條數據 或 多條
@Delete
void delete(Person... persons);
//一次更新單條數據 或 多條
@Update
void update(Person... persons);
//根據字段去查找數據
@Query("SELECT * FROM person WHERE uid= :uid")
Person getPersonByUid(int uid);
//一次查找多個數據
@Query("SELECT * FROM person WHERE uid IN (:userIds)")
List<Person> loadAllByIds(List<Integer> userIds);
//多個條件查找
@Query("SELECT * FROM person WHERE name = :name AND age = :age")
Person getPersonByNameage(String name, int age);
}
複製代碼
這裏惟一特殊的就是@Insert。其有一段介紹:對數據庫設計時,不容許重複數據的出現。不然,必然形成大量的冗餘數據。實際上,不免會碰到這個問題:衝突。當咱們像數據庫插入數據時,該數據已經存在了,必然形成了衝突。該衝突該怎麼處理呢?在@Insert註解中有conflict用於解決插入數據衝突的問題,其默認值爲OnConflictStrategy.ABORT。對於OnConflictStrategy而言,它封裝了Room解決衝突的相關策略。
這裏好比在插入的時候咱們加上了OnConflictStrategy.REPLACE,那麼往已經有uid=1的person表裏再插入uid =1的person數據,那麼新數據會覆蓋就數據。若是咱們什麼都不加,那麼久是默認的OnConflictStrategy.ABORT,重複上面的動做,你會發現,程序崩潰了。也就是上面說的終止事務。其餘你們能夠本身試試
直接上代碼
//註解指定了database的表映射實體數據以及版本等信息(後面會詳細講解版本升級)
@Database(entities = {Person.class, Clothes.class}, version = 1)
public abstract class AppDataBase extends RoomDatabase {
public abstract PersonDao getPersonDao();
public abstract ClothesDao getClothesDao();
}
複製代碼
Room建立咱們的AppDataBase,咱們把它封裝成單例,省的每次都去執行一遍,耗性能
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(){
if(appDataBase==null){
synchronized (DBInstance.class){
if(appDataBase==null){
appDataBase = Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
//下面註釋表示容許主線程進行數據庫操做,可是不推薦這樣作。
//我這裏是爲了Demo展現,稍後會結束和LiveData和RxJava的使用
.allowMainThreadQueries()
.build();
}
}
}
return appDataBase;
}
}
複製代碼
作完這一切,那麼咱們的準備工做就作完了。讓咱們來插入一條數據
Person person_ = new Person("Room", 18);
DBInstance.getInstance().getPersonDao().insert(person_);
複製代碼
這裏怎麼查看db數據呢?首先咱們把db文件存在手機內存裏,記得打開存儲權限,就是在上面代碼裏指定路徑
private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
插入數據後,就會在手機內存卡生成db文件。
拿到db文件,怎麼辦呢。用插件!!Database Navigator,插件教程
這裏的意思好比我已經往person表存裏數據。可是我要增長字段,或者是增長索引。若是你直接寫上去,你會發現,你再使用數據庫的時候,會直接崩潰。怎麼辦呢,用過greendao的人都知道,咱們要升級數據庫版本
@Entity
public class Person {
//...省略部分代碼,便於理解。
//這裏給Person加上一個兒子
}
複製代碼
而後來到咱們的Database類裏,把版本信息改下,並增添一個Migration 類,告訴Room是哪張表改了什麼東西
//修改版本信息爲2
@Database(entities = {Person.class, Clothes.class}, version = 2)
public abstract class AppDataBase extends RoomDatabase {
public abstract PersonDao getPersonDao();
public abstract ClothesDao getClothesDao();
//數據庫變更添加Migration,簡白的而說就是版本1到版本2改了什麼東西
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//告訴person表,增添一個String類型的字段 son
database.execSQL("ALTER TABLE person ADD COLUMN son TEXT");
}
};
}
複製代碼
關於版本更新的execSQL裏的用法,能夠參考Room升級。也能夠自行度娘,網上不少
最後來到咱們的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(){
if(appDataBase==null){
synchronized (DBInstance.class){
if(appDataBase==null){
return Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
.allowMainThreadQueries()
//加上版本升級信息
.addMigrations(AppDataBase.MIGRATION_1_2)
.build();
}
}
}
return appDataBase;
}
}
複製代碼
作完以上操做後,咱們來運行下項目看看。成功,打開數據看看(本文demo裏,我把升級代碼註釋了,想測試的可自行打開):
首先看咱們DBInstance裏的Room建立咱們的AppDataBase,這句代碼
//下面註釋表示容許主線程進行數據庫操做,可是不推薦這樣作。
.allowMainThreadQueries()
複製代碼
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.
這個時候,咱們來結合RxJava來使用下,這樣數據操做能夠放在子線程,回調能夠切換到主線程更改UI。首先是引入咱們的依賴
implementation 'android.arch.persistence.room:rxjava2:2.1.4'
//下面這個是配合rxjava使用的
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
複製代碼
這裏須要注意2點:
一、在數據庫執行@Insert、@Delete、@Update操做時候可使用(注意是可使用)RxJava裏的類型有:Completable,Single,Maybe
二、在執行@Query操做時,能夠返回的類型有:Single,Maybe,Observable,Flowable
這裏須要注意:
這裏可能你們對Single,Maybe,Completable,Observable,Flowable不大瞭解,這裏作個簡單介紹:
一、Completable:只有onComplete和onError方法,即只有「完成」和「錯誤」兩種狀態,不會返回具體的結果。
二、Single:其回調爲onSuccess和onError,查詢成功會在onSuccess中返回結果,須要注意的是,若是未查詢到結果,即查詢結果爲空,會直接走onError回調,拋出EmptyResultSetException異常。
三、Maybe:其回調爲onSuccess,onError,onComplete,查詢成功,若是有數據,會先回調onSuccess再回調onComplete,若是沒有數據,則會直接回調onComplete。
四、Flowable/Observable:這是返回一個可觀察的對象,查詢的部分有變化時,都會回調它的onNext方法,沒有數據變化的話,不回調。直到Rx流斷開。
錯誤: Methods annotated with @Insert can return either void, long, Long, long[], Long[] or List.
因此如今好多網上關於這部分,也沒有講清楚。若是有清楚的同窗請指正。請看Dao類:
@Dao
public interface DogDao {
//返回值是插入成功的行id
@Insert
List<Long> insert(Dog... dogs);
@Delete
void delete(Dog... dogs);
//返回刪除的行id
@Delete
int delete(Dog dog);
@Update
void update(Dog... dogs);
@Update
int update(Dog dog);
//查詢全部對象 且 觀察數據。用背壓Flowable能夠實現,若是須要一次性查詢,能夠用別的類型
@Query("Select * from dog")
Flowable<List<Dog>> getAll();
//刪除所有數據
@Query("DELETE FROM dog")
void deleteAll();
//根據字段去查找數據
@Query("SELECT * FROM dog WHERE id= :id")
Single<Dog> getDogById(int id);
}
複製代碼
讓咱們在代碼裏,用可觀察的背壓,去實時查詢咱們的所有dog。這裏只要調用一次,以後數據有更新的時候,會自動走這個觀察者回調。
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("當前狗狗總數" + dogs.size());
}
});
複製代碼
那不少人問了。@Insert、@Delete、@Update這些該怎麼辦。不少博客都是把返回值寫在Dao裏。真實運行起來,直接報錯。因此這裏要在代碼中使用RxJava。用於項目的時候最好封裝起來。好比用Single插入數據:(這裏用哪一個類型呢,徹底根據你的需求而定,好比插入數據後,我要知道插入的行id的是多少,就不能用Completable,由於他沒有返回值,這個仍是靈活運用的)
Single.fromCallable(new Callable<List<Long>>() {
@Override
public List<Long> call() 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) {
}
//一次插入多條數據,返回的行id的集合
@Override
public void onSuccess(List<Long> o) {
for (Long data : o) {
LogUtils.i("使用Single插入數據", "onSuccess ==> " + data);
}
}
@Override
public void onError(Throwable e) {
LogUtils.i("使用Single插入數據", "onError");
}
});
複製代碼
若是你不須要觀察者回調,能夠直接。
Single.fromCallable(new Callable<List<Long>>() {
@Override
public List<Long> call() throws Exception {
Dog dog = new Dog();
return DBInstance.getInstance().getDogDao().insert(dog);
}
}).subscribeOn(Schedulers.io())
.subscribe();
複製代碼
效果以下(查詢一次後,更新數據庫,都是獲得數據庫裏最新數據):
這裏咱們在DogDao中添加LiveData的返回值,(查詢範圍id裏dog的值)
@Query("SELECT * FROM dog WHERE id>= :minId AND id<= :maxId")
LiveData<List<Dog>> getToLiveData(int minId, int maxId);
複製代碼
Activity裏的代碼:
DBInstance.getInstance().getDogDao().getToLiveData(2, 12).observe(this, new Observer<List<Dog>>() {
@Override
public void onChanged(List<Dog> dogs) {
ToastUtils.showToast("查出來的當前值 ==> " + dogs.size());
}
});
複製代碼
還記得咱們以前講的LiveData嗎。這個時候,LiveData跟隨生命週期的。onChanged只會在激活狀態下回調,若是銷燬了,那麼將會取消觀察者。