Android 讓你的 Room 搭上 RxJava 的順風車 從重複的程式碼中解脫出來
什麼是 Room ?
谷歌為了幫助開發者解決 Android 架構設計問題,在 Google I/O 2017 釋出一套幫助開發者解決 Android 架構設計的方案:Android Architecture Components,而我們的 Room 正是這套方案的兩大模組之一。
- 定義:資料庫解決方案
- 組成:Database、Entity、DAO
為什麼本文叫谷歌範例?
為了方便開發者進行學習和理解,Google 在 GitHub 上上傳了一系列的 Android Architecture Components 開原始碼:googlesamples/android-architecture-components 本文就是通過解析這套範例的第一部分:BasicRxJavaSample 來對 Room 的使用進行分析。
關於本文中的程式碼以及後續文章中的程式碼,我已經上傳至我的 GitHub 歡迎大家圍觀、star
詳見-> FishInWater-1999/ArchitectureComponentsStudy
開始之前
為什麼我們要學 Room
相比於我們直接使用傳統方式,如果直接使用 Java
程式碼進行 SQLite
操作,每次都需要手寫大量重複的程式碼,對於我們最求夢想的程式設計師來說,這種無聊的過程簡直是一種折磨。於是,Room
也就應運而生了
- 它通過註解處理器的形式,將繁瑣無趣的程式碼封裝起來,我們只需要新增一個簡單的註解,就可以完成一系列複雜的功能!
首先我們需要了解下
Room
的基本組成
前面我們已經說過 Room 的使用,主要由 Database、Entity、DAO 三大部分組成,那麼這三大組成部分又分別是什麼呢?
- Database:建立一個由 Room 管理的資料庫,並在其中自定義所需要操作的資料庫表
要求:
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 {...}
# BasicRxJavaSample 原始碼解析
由於是原始碼解析,那我就以:從基礎的類開始,一層層向上,抽絲剝繭,最後融為一體的方式,給大家進行解析。那麼現在就讓我們開始吧。
表的搭建
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")
中傳入的資料表 )
我們再往下看:
- @ColumnInfo(name = "userid") :該註解註解的資料成員,將會在表中生成相應的名為:
userid
的列 - @PrimaryKey :顧名思義該註解與
@ColumnInfo(name = "...")
註解一起使用,表示表中的主鍵,這裡要注意一點,在@Entity
的原始碼中強調:Each entity must have at least 1 field annotated with {@link PrimaryKey}. 也就是說一個被@Entity(...)
標註的資料表類中至少要有一個主鍵 - @Ignore :被該註解註釋的資料成員、方法,將會被註解處理器忽略,不進行處理
這裡我們發現,程式碼中有存在兩個構造方法,為什麼 GoogleSample 中會存在這種看似多此一舉的情況呢?我們再仔細觀察就會發想,上方的構造方法標記了 @Ignore
標籤,而下方的構造方法卻沒有。由於在 @Entity
標註的類中,構造方法和列屬性的 get()
方法都會被註解處理器自動識別處理。我們就不難想到,Google 之所以這樣設計,是因為我們於是需要建立臨時的 User
物件,但我們又不希望 @Entity
在我們呼叫構造方法時,就將其存入資料庫。所以我們就有了這個被 @Ignore
的構造方法,用於建立不被自動存入資料庫的臨時物件,等到我們想將這個物件存入資料庫時,呼叫User(String id, String userName)
即可。
UserDao
上面我們通過 @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
相當於保留表的結構,重新建立了這個表,所以刪除後不記錄日誌,不可以恢復資料。
UsersDatabase
有關於 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 物件,這一篇就夠了
UserDataSource
我們可以看到:絕大多數的資料庫操作方法,都定義在了 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
一摸一樣,這裡我們就不贅述了。
LocalUserDataSource
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
物件,進行資料庫的增刪改查操作。
- 到此為止,有關於 Room 對資料庫的操作部分就講完了,接下來我們進行檢視層搭建的解析。
UserViewModel
首先我們先實現 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
物件傳入,從而對他的方法進行相應的回撥,也就是先實現了所需的資料庫操作。每個方法的功能,我已經在註釋中給出,這裡就不再贅述
ViewModelFactory
有上面我們可以看到,我們已經有了進行資料處理的 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
關於 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 的呼叫
- 由於
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 結束後,訂閱仍在進行的情況
onCreate
控制元件、資料來源層、資料庫等的初始化
@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
方法如下
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
執行緒中訪問資料庫進行修改 - 切換到主執行緒進行相應處理,比如讓按鈕恢復到可點選狀態
onStart
初始化使用者資訊,修改 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
資訊
onStop
取消訂閱
@Override
protected void onStop() {
super.onStop();
// 取消訂閱。防止在 activity 或者 fragment 銷燬後仍然佔用著記憶體,無法釋放。
mDisposable.clear();
}
- 通過我們之前例項化的
CompositeDisposable
物件,解除訂閱關係
原始碼
Demo 地址
ArchitectureComponentsStudy
總結
學會使用 Android Architecture Components
提供的元件簡化我們的開發,能夠使我們開發的應用模組更解耦更穩定,檢視與資料持久層分離,以及更好的擴充套件性與靈活性。最後,碼字不易,別忘了點個關注