谷歌为了帮助开发者解决 Android 架构设计问题,在 Google I/O 2017 发布一套帮助开发者解决 Android 架构设计的方案:Android Architecture Components,而咱们的 Room 正是这套方案的两大模块之一。java
为了方便开发者进行学习和理解,Google 在 GitHub 上上传了一系列的 Android Architecture Components 开源代码:googlesamples/android-architecture-components 本文就是经过解析这套范例的第一部分:BasicRxJavaSample 来对 Room 的使用进行分析。android
关于本文中的代码以及后续文章中的代码,我已经上传至个人 GitHub 欢迎你们围观、star 详见-> FishInWater-1999/ArchitectureComponentsStudygit
相比于咱们直接使用传统方式,若是直接使用 Java
代码进行 SQLite
操做,每次都须要手写大量重复的代码,对于咱们最求梦想的程序员来讲,这种无聊的过程简直是一种折磨。因而,Room
也就应运而生了程序员
首先咱们须要了解下
Room
的基本组成github
前面咱们已经说过 Room 的使用,主要由 Database、Entity、DAO 三大部分组成,那么这三大组成部分又分别是什么呢?数据库
1. 必须是abstract类并且的extends RoomDatabase。
2. 必须在类头的注释中包含与数据库关联的实体列表(Entity对应的类)。
3. 包含一个具备0个参数的抽象方法,并返回用@Dao注解的类。
复制代码
经过单例模式实现,你能够经过静态 getInstance(...) 方法,获取数据库实例:数组
public static UsersDatabase getInstance(Context context)
安全
Entity:数据库中,某个表的实体类,如: @Entity(tableName = "users")
public class User {...}
多线程
DAO:具体访问数据库的方法的接口 @Dao
public interface UserDao {...}
架构
因为是源码解析,那我就以:从基础的类开始,一层层向上,抽丝剥茧,最后融为一体的方式,给你们进行解析。那么如今就让咱们开始吧。
Room 做为一个 Android 数据库操做的注解集合,最基本操做就是对咱们数据库进行的。因此,先让咱们试着创建一张名为 “users” 的数据表
/** * 应用测试的表结构模型 */
@Entity(tableName = "users")// 表名注解
public class User {
/** * 主键 * 因为主键不能为空,因此须要 @NonNull 注解 */
@NonNull
@PrimaryKey
@ColumnInfo(name = "userid")// Room 列注解
private String mId;
/** * 用户名 * 普通列 */
@ColumnInfo(name = "username")
private String mUserName;
/** * 构造方法 * 设置为 @Ignore 将其忽视 * 这样以来,这个注解方法就不会被传入 Room 中,作相应处理 * @param mUserName */
@Ignore
public User(String mUserName){
this.mId = UUID.randomUUID().toString();
this.mUserName = mUserName;
}
/** * 咱们发现与上个方法不一样,该方法没有标记 @Ignore 标签 * * 因此编译时该方法会被传入 Room 中相应的注解处理器,作相应处理 * 这里的处理应该是 add 新数据 * @param id * @param userName */
public User(String id, String userName) {
this.mId = id;
this.mUserName = userName;
}
public String getId() {
return mId;
}
public String getUserName() {
return mUserName;
}
}
复制代码
首先在表头部分,咱们就见到了以前说过的 @Entity(...)
标签,以前说过该标签表示数据库中某个表的实体类,咱们查看它的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Entity {...}
复制代码
从中咱们能够知道该注解实在编译注解所在的类时触发的,这是咱们注意到 Google 对该类的介绍是:
Marks a class as an entity. This class will have a mapping SQLite table in the database.
复制代码
由此可知当注解所在的类,好比咱们的这个 User
类编译时,相应的注解处理器就会调用其内部相应的代码,创建一个名为 users
(在 @Entity(tableName = "users")
中传入的数据表 )
咱们再往下看:
userid
的列@ColumnInfo(name = "...")
注解一块儿使用,表示表中的主键,这里要注意一点,在 @Entity
的源码中强调:Each entity must have at least 1 field annotated with {@link PrimaryKey}. 也就是说一个被 @Entity(...)
标注的数据表类中至少要有一个主键这里咱们发现,代码中有存在两个构造方法,为何 GoogleSample 中会存在这种看似画蛇添足的状况呢?咱们再仔细观察就会发想,上方的构造方法标记了 @Ignore
标签,而下方的构造方法却没有。因为在 @Entity
标注的类中,构造方法和列属性的 get()
方法都会被注解处理器自动识别处理。咱们就不难想到,Google 之因此这样设计,是由于咱们因而须要建立临时的 User
对象,但咱们又不但愿 @Entity
在咱们调用构造方法时,就将其存入数据库。因此咱们就有了这个被 @Ignore
的构造方法,用于建立不被自动存入数据库的临时对象,等到咱们想将这个对象存入数据库时,调用User(String id, String userName)
便可。
上面咱们经过 @Entity
创建了一张 users
表,下面就让咱们用 @Dao
注解来变写 UserDao
接口。
@Dao
public interface UserDao {
/** * 为了简便,咱们只在表中存入1个用户信息 * 这个查询语句能够得到 全部 User 但咱们只须要第一个便可 * @return */
@Query("SELECT * FROM Users LIMIT 1")
Flowable<User> getUser();
/** * 想数据库中插入一条 User 对象 * 若数据库中已存在,则将其替换 * @param user * @return */
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable insertUser(User user);
/** * 清空全部数据 */
@Query("DELETE FROM Users")
void deleteAllUsers();
}
复制代码
按照咱们正常编写的习惯,咱们会在该类中,编写相应的数据库操做代码。但与之不一样的是采用 Room
以后,咱们将其变为一个接口类,而且只须要编写和设定相应的标签便可,不用再去关心存储操做的具体实现。
/** * 为了简便,咱们只在表中存入1个用户信息 * 这个查询语句能够得到 全部 User 但咱们只须要第一个便可 * @return */
@Query("SELECT * FROM Users LIMIT 1")
Flowable<User> getUser();
复制代码
这里咱们看到,该查询方法使用的是 @Query
注解,那么这个注解的具体功能是什么呢?Google 官方对它的解释是:在一个被标注了 @Dao
标签的类中,用于查询的方法。顾名思义被该注解标注的方法,会被 Room
的注解处理器识别,看成一个数据查询方法,至于具体的查询逻辑并不须要咱们关心,咱们只须要将 SQL 语句
做为参数,传入 @Query(...)
中便可。以后咱们发现,该方法返回的是一个背压 Flowable<...>
类型的对象,这是为了防止表中数据过多,读取速率远大于接收数据,从而致使内存溢出的问题,具体详见 RxJava
的教程,这里我就不赘述了。
/** * 想数据库中插入一条 User 对象 * 若数据库中已存在,则将其替换 * @param user * @return */
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable insertUser(User user);
复制代码
咱们看到,上述方法被 @Insert
注解所标注,从名字就能看出,这将会是一个插入方法。顾名思义被 @Insert
标注的方法,会用于向数据库中插入数据,惟一让咱们迷茫的是括号中的这个 onConflict
参数,onConflict
意为“冲突”,再联想下咱们平常生活中的数据库操做,就不难想到:这是用来设定,当插入数据库中的数据,与原数据发生冲突时的处理方法。这里咱们传入的是 OnConflictStrategy.REPLACE
,意为“若是数据发生冲突,则用其替换掉原数据”,除此以外还有不少相应操做的参数,好比ROLLBACK
ABORT
等,篇幅缘由就不详细说明了,你们能够自行查阅官方文档。还有一点值得说的是这个 Completable
,该返回值是 RxJava
的基本类型,它只处理 onComplete
onError
事件,能够当作是Rx的Runnable。
/** * 清空全部数据 */
@Query("DELETE FROM Users")
void deleteAllUsers();
复制代码
最后这个方法就是清空 users
表中的全部内容,很简单,这里就不作说明了。惟一须要注意的是,这里使用了 DELETE FROM 表名
的形式,而不是 truncate table 表名
,区别就在于:效率上truncate
比delete
快,但truncate
至关于保留表的结构,从新建立了这个表,因此删除后不记录日志,不能够恢复数据。
有关于 Room
的三大组成咱们已经讲完了两个,如今就让咱们看看最后一个 @Database
注解:
@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class UsersDatabase extends RoomDatabase {
/** * 单例模式 * volatile 确保线程安全 * 线程安全意味着改对象会被许多线程使用 * 能够被看做是一种 “程度较轻的 synchronized” */
private static volatile UsersDatabase INSTANCE;
/** * 该方法因为得到 DataBase 对象 * abstract * @return */
public abstract UserDao userDao();
public static UsersDatabase getInstance(Context context) {
// 若为空则进行实例化
// 不然直接返回
if (INSTANCE == null) {
synchronized (UsersDatabase.class) {
if (INSTANCE == null){
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.build();
}
}
}
return INSTANCE;
}
}
复制代码
老样子, Google
定义中是这么写的:将一个类标记为 Room
数据库。顾名思义,咱们须要在标记了该标签的类里,作具体的数据库操做,好比数据库的创建、版本更新等等。咱们看到,咱们向其中传入了多个参数,包括:entities
以数组结构,标记一系列数据库中的表,这个例子中咱们只有一个 User
表,因此只传入一个; version
数据库版本;exportSchema
用于历史版本库的导出
/** * 单例模式 * volatile 确保线程安全 * 线程安全意味着改对象会被许多线程使用 * 能够被看做是一种 “程度较轻的 synchronized” */
private static volatile UsersDatabase INSTANCE;
复制代码
能够看出这是一个单例模式,用于建立一个全局可得到的 UsersDatabase 对象。
public static UsersDatabase getInstance(Context context) {
// 若为空则进行实例化
// 不然直接返回
if (INSTANCE == null) {
synchronized (UsersDatabase.class) {
if (INSTANCE == null){
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.build();
}
}
}
return INSTANCE;
}
复制代码
这是单例模式对象 INSTANCE 的得到方法,不明白的同窗能够去看我这篇 单例模式-全局可用的 context 对象,这一篇就够了
咱们能够看到:绝大多数的数据库操做方法,都定义在了 UserDao
中,虽然通常注解类的方法不会被继承,可是有些被特殊标记的方法可能会被继承,可是咱们以后要创建的不少功能类中,都须要去调用 UserDao
里的方法。因此咱们这里定义 UserDataSource
接口:
public interface UserDataSource {
/** * 从数据库中读取信息 * 因为读取速率可能 远大于 观察者处理速率,故使用背压 Flowable 模式 * Flowable:https://www.jianshu.com/p/ff8167c1d191/ */
Flowable<User> getUser();
/** * 将数据写入数据库中 * 若是数据已经存在则进行更新 * Completable 能够看做是 RxJava 的 Runnale 接口 * 但他只能调用 onComplete 和 onError 方法,不能进行 map、flatMap 等操做 * Completable:https://www.jianshu.com/p/45309538ad94 */
Completable insertOrUpdateUser(User user);
/** * 删除全部表中全部 User 对象 */
void deleteAllUsers();
}
复制代码
该接口很简单,就是一个工具,方法和 UserDao
一摸同样,这里咱们就不赘述了。
public class LocalUserDataSource implements UserDataSource {
private final UserDao mUserDao;
public LocalUserDataSource(UserDao userDao) {
this.mUserDao = userDao;
}
@Override
public Flowable<User> getUser() {
return mUserDao.getUser();
}
@Override
public Completable insertOrUpdateUser(User user) {
return mUserDao.insertUser(user);
}
@Override
public void deleteAllUsers() {
mUserDao.deleteAllUsers();
}
}
复制代码
咱们先看看官方的解析:“使用 Room
数据库做为一个数据源。”即经过该类的对象所持有的 UserDao
对象,进行数据库的增删改查操做。
首先咱们先实现 ViewModel
类,那什么是 ViewModel
类呢?从字面上理解的话,它确定是跟视图 View
以及数据 Model
相关的。其实正像它字面意思同样,它是负责准备和管理和UI组件 Fragment/Activity
相关的数据类,也就是说 ViewModel
是用来管理UI相关的数据的,同时 ViewModel
还能够用来负责UI组件间的通讯。那么如今就来看看他的具体实现:
public class UserViewModel extends ViewModel {
/** * UserDataSource 接口 */
private final UserDataSource mDataSource;
private User mUser;
public UserViewModel(UserDataSource dataSource){
this.mDataSource = dataSource;
}
/** * 从数据库中读取全部 user 名称 * @return 背压形式发出全部 User 的名字 * * 因为数据库中 User 量可能很大,可能会由于背压致使内存溢出 * 故采用 Flowable 模式,取代 Observable */
public Flowable<String> getUserName(){
return mDataSource.getUser()
.map(new Function<User, String>() {
@Override
public String apply(User user) throws Exception {
return user.getUserName();
}
});
}
/** * 更新/添加 数据 * * 判断是否为空,若为空则建立新 User 进行存储 * 若不为空,说明该 User 存在,这得到其主键 'getId()' 和传入的新 Name 拼接,生成新 User 存储 * 经过 insertOrUpdateUser 接口,返回 Comparable 对象,监听是否存储成功 * @param userName * @return */
public Completable updateUserName(String userName) {
mUser = mUser == null
? new User(userName)
: new User(mUser.getId(), userName);
return mDataSource.insertOrUpdateUser(mUser);
}
}
复制代码
代码结构很是简单,mDataSource
就是咱们前面创建的 UserDataSource
接口对象,因为咱们的数据库操做控制类:LocalUserDataSource
是经过是实现该接口的,因此咱们就能够在外部将 LocalUserDataSource
对象传入,从而对他的方法进行相应的回调,也就是先实现了所需的数据库操做。每一个方法的功能,我已经在注释中给出,这里就再也不赘述
有上面咱们能够看到,咱们已经有了进行数据处理的 ViewModel
类,那么咱们这里的 ViewModelFactory
类又有什么做用呢?让咱们先看下范例中的实现:
public class ViewModelFactory implements ViewModelProvider.Factory {
private final UserDataSource mDataSource;
public ViewModelFactory(UserDataSource dataSource) {
mDataSource = dataSource;
}
// 你须要经过 ViewModelProvider.Factory 的 create 方法来建立(自定义的) ViewModel
// 参考文档:https://medium.com/koderlabs/viewmodel-with-viewmodelprovider-factory-the-creator-of-viewmodel-8fabfec1aa4f
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
// 为何这里用 isAssignableFrom 来判断传入的 modelClass 类的类型, 而不直接用 isInstance 判断?
// 答:两者功能同样,但若是传入值(modelClass 为空)则 isInstance 会报错奔溃,而 isAssignableFrom 不会
if (modelClass.isAssignableFrom(UserViewModel.class)) {
return (T) new UserViewModel(mDataSource);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}
复制代码
ViewModelFactory
继承自 ViewModelProvider.Factory
,它负责帮你建立 ViewModel
实例。但你也许会问,咱们不是已经有了 ViewModel
的构造方法了吗?在用 ViewModelFactory
不是画蛇添足?若是还不熟悉 ViewModelFactory
有关内容的,能够看下这篇:ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
关于 Injection
,这是个帮助类,它和 Room 的逻辑功能并无关系。Sample
中将其独立出来用于各个对象、类型的注入,先让咱们看下该类的实现:
public class Injection {
/** * 经过该方法实例化出能操做数据库的 LocalUserDataSource 对象 * @param context * @return */
public static UserDataSource provideUserDateSource(Context context) {
// 得到 RoomDatabase
UsersDatabase database = UsersDatabase.getInstance(context);
// 将可操做 UserDao 传入
// 实例化出可操做 LocalUserDataSource 对象方便对数据库进行操做
return new LocalUserDataSource(database.userDao());
}
/** * 得到 ViewModelFactory 对象 * 为 ViewModel 实例化做准备 * @param context * @return */
public static ViewModelFactory provideViewModelFactory(Context context) {
UserDataSource dataSource = provideUserDateSource(context);
return new ViewModelFactory(dataSource);
}
}
复制代码
该类有两个方法组成,实现了各个类型数据相互间的转换,想再让咱们先看下第一个方法:
/** * 经过该方法实例化出能操做数据库的 LocalUserDataSource 对象 * @param context * @return */
public static UserDataSource provideUserDateSource(Context context) {
// 得到 RoomDatabase
UsersDatabase database = UsersDatabase.getInstance(context);
// 将可操做 UserDao 传入
// 实例化出可操做 LocalUserDataSource 对象方便对数据库进行操做
return new LocalUserDataSource(database.userDao());
}
复制代码
在该方法中,咱们首先接到了咱们的 context
对象,经过 UsersDatabase.getInstance(context)
方法,让 database
持有 context
,实现数据库的连接和初始化。同时放回一个 LocalUserDataSource
对象,这样一来咱们就能够对数据表中的内容惊醒相应的操做。
/** * 得到 ViewModelFactory 对象 * 为 ViewModel 实例化做准备 * @param context * @return */
public static ViewModelFactory provideViewModelFactory(Context context) {
UserDataSource dataSource = provideUserDateSource(context);
return new ViewModelFactory(dataSource);
}
复制代码
该方法的功能很是明确,就是为咱们实例化出一个 ViewModelFactory
对象,为咱们日后建立 ViewModel
做准备。能够看到,这里咱们调用了前面的 provideUserDateSource
方法,经过该方法得到了对数据库操做的 LocalUserDataSource
对象,这里咱们就看到了单例模式使用的先见性,使得数据库不会被反复的建立、链接。
UserActivity
的内容较多我就不贴完整的代码,咱们逐步进行讲解首先咱们准备了所需的给类数据成员:
private static final String TAG = UserActivity.class.getSimpleName();
private TextView mUserName;
private EditText mUserNameInput;
private Button mUpdateButton;
// 一个 ViewModel 用于得到 Activity & Fragment 实例
private ViewModelFactory mViewModelFactory;
// 用于访问数据库
private UserViewModel mViewModel;
// disposable 是订阅事件,能够用来取消订阅。防止在 activity 或者 fragment 销毁后仍然占用着内存,没法释放。
private final CompositeDisposable mDisposable = new CompositeDisposable();
复制代码
mViewModelFactory
、 mViewModel
两个数据成员,用于负责数据源的操做CompositeDisposable
对象,用于管理订阅事件,防止 Activity 结束后,订阅仍在进行的状况控件、数据源层、数据库等的初始化
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
mUserName = findViewById(R.id.user_name);
mUserNameInput = findViewById(R.id.user_name_input);
mUpdateButton = findViewById(R.id.update_user);
// 实例化 ViewModelFactory 对象,准备实例化 ViewModel
mViewModelFactory = Injection.provideViewModelFactory(this);
mViewModel = new ViewModelProvider(this, mViewModelFactory).get(UserViewModel.class);
mUpdateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateUserName();
}
});
}
复制代码
ViewModel
的初始化,在这过程当中,也就实现了数据库的连接updateUserName
方法以下修改数据库中用户信息
private void updateUserName() {
String userName = mUserNameInput.getText().toString();
// 在完成用户名更新以前禁用“更新”按钮
mUpdateButton.setEnabled(false);
// 开启观察者模式
// 更新用户信息,结束后从新开启按钮
mDisposable.add(mViewModel.updateUserName(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action() {
@Override
public void run() throws Exception {
mUpdateButton.setEnabled(true);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "accept: Unable to update username");
}
}));
}
复制代码
io
线程中访问数据库进行修改初始化用户信息,修改 UI
界面内容
@Override
protected void onStart() {
super.onStart();
// 观察者模式
// 经过 ViewModel 从数据库中读取 UserName 显示
// 若是读取失败,显示错误信息
mDisposable.add(mViewModel.getUserName()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
mUserName.setText(s);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, "Unable to update username");
}
}));
}
复制代码
io
线程中进行数据库访问UI
信息取消订阅
@Override
protected void onStop() {
super.onStop();
// 取消订阅。防止在 activity 或者 fragment 销毁后仍然占用着内存,没法释放。
mDisposable.clear();
}
复制代码
CompositeDisposable
对象,解除订阅关系Android Architecture Components
提供的 Room
组件简化了咱们的开发,可以使咱们开发的应用模块更解耦更稳定,视图层与数据持久层分离,以及更好的扩展性与灵活性。最后,码字不易,别忘了点个关注哦Android
干货,感兴趣的战友别忘了关注哦 _yuanhao 的安卓进阶相关好文推荐
ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
单例模式-全局可用的 context 对象,这一篇就够了
缩放手势 ScaleGestureDetector 源码解析,这一篇就够了
Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了
看完这篇再不会 View 的动画框架,我跪搓衣板
Android 自定义时钟控件 时针、分针、秒针的绘制这一篇就够了
android 自定义控件之-绘制钟表盘
Android 进阶自定义 ViewGroup 自定义布局
Android 逐帧动画( Drawable 动画),这一篇就够了
看完这篇还不会自定义 View ,我跪搓衣板
按期分享Android开发
湿货,追求文章幽默与深度
的完美统一。