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 表名 ,區別就在於:效率上truncatedelete快,但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();
  • 首先界面操作的各個控件
  • 接這就是 mViewModelFactorymViewModel 兩個數據成員,用於負責數據源的操作
  • 再就是一個 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 提供的組件簡化我們的開發,能夠使我們開發的應用模塊更解耦更穩定,視圖與數據持久層分離,以及更好的擴展性與靈活性。最后,碼字不易,別忘了點個關注哦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM