在真正接触并使用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只会在激活状态下回调,若是销毁了,那么将会取消观察者。