谷歌爲了幫助開發者解決 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
提供的組件簡化咱們的開發,可以使咱們開發的應用模塊更解耦更穩定,視圖與數據持久層分離,以及更好的擴展性與靈活性。最後,碼字不易,別忘了點個關注哦