1. 程式人生 > >Android Architecture Components 整理

Android Architecture Components 整理

Android Architecture Components是谷歌在Google I/O 2017釋出一套幫助開發者解決Android架構設計的方案。

裡面包含了兩大塊內容:

  1. 生命週期相關的Lifecycle-aware Components
  2. 資料庫解決方案Room

A new collection of libraries that help you design robust, testable, and maintainable apps.

即這是一個幫助構建穩定,易於測試和易於維護的App架構的庫。

 

ViewModel

ViewModel是為特定的UI(例如Activity/Fragment

)提供資料的類,同時它也承擔和資料相關的業務邏輯處理功能:比如根據uid請求網路獲取完整的使用者資訊,或者將使用者修改的頭像上傳到伺服器。因為ViewModel是獨立於View(例如Activity/Fragment)這一層的,所以並不會被View層的事件影響,比如Activity被回收或者螢幕旋轉等並不會造成ViewModel的改變。

 

 

LiveData

LiveData是一個包含可以被觀察的資料載體。這麼說又有點不好理解了,其實他就是基於觀察者模式去做的。當LiveData的資料發生變化時,所有對這個LiveData變化感興趣的類都會收到變化的更新。並且他們之間並沒有類似於介面回撥這樣的明確的依賴關係。LiveData還能夠感知元件(例如activities, fragments, services

)的生命週期,防止記憶體洩漏。其實這和RxJava非常相似,但是LiveData能夠在元件生命週期結束後自動阻斷資料流的傳播,防止產生空指標等意外。這個RxJava是不同的。

 

 

獲取資料

用Retrofit的WebService獲取資料

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

Repository

Repository類的作用就是獲取並提供各種來源的資料(資料庫中的資料,網路資料,快取資料等),並且在來源資料更新時通知資料的獲取方。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below(這不是最好的實現,以後會修復的)
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity(為了簡單起見,簡化了錯誤處理的情況)
                data.setValue(response.body());
            }
        });
        return data;
    }
}

連線ViewModel和Repository

 

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

 

快取資料

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity(簡單的記憶體快取)
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

持久化資料

Room

Room是一個物件對映庫,(姑且按GreenDAO的功能來理解吧)可以在在編譯的時候就能檢測出SQL語句的異常。還能夠在資料庫內容發生改變時通過LiveData的形式發出通知

 

  1. 定義User類,並且使用@Entity註解:   

  

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}

  2.建立RoomDatabase:

 

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

3.建立DAO

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

4.在RoomDatabase中訪問DAO

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

 

把Room和UserRepository結合起來:

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.(從資料庫中直接返回LiveData)
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread(在後臺執行緒執行)
            // check if user was fetched recently(檢查資料庫中是否有資料)
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database(不用其他程式碼就能實現資料自動更新)
                userDao.save(response.body());
            }
        });
    }
}

資料來源的唯一性

在上面提供的程式碼中,資料庫是App資料的唯一來源。Google推薦採用這種方式。

最終的架構形態

下面這張圖展示了使用Android Architecture Components來構建的App整體的架構:

 

 

一些App架構設計的推薦準則

  • 不要把在Manifest中定義的元件作為提供資料的來源(包括Activity、Services、Broadcast Receivers等),因為他們的生命週期相對於App的生命週期是相對短暫的。
  • 嚴格的限制每個模組的功能。比如上面提到的不要再ViewModel中增加如何獲取資料的程式碼。
  • 每個模組儘可能少的對外暴露方法。
  • 模組中對外暴露的方法要考慮單元測試的方便。
  • 不要重複造輪子,把精力放在能夠讓App脫穎而出的業務上。
  • 儘可能多的持久化資料,因為這樣即使是在網路條件不好的情況下,使用者仍然能夠使用App
  • 保證資料來源的唯一性(即:提供一個類似UserRepository的類)


Lifecycle

Lifecycle是一個包含元件(Activity或者Fragment)生命週期狀態的類,這個類還能夠為其他類提供當前的生命週期。

Lifecycle使用兩個主要的列舉來跟蹤他所關聯元件的生命週期。

  • Event 事件 從元件或者Lifecycle類分發出來的生命週期,它們和Activity/Fragment生命週期的事件一一對應。(ON_CREATE,ON_START,ON_RESUME,ON_PAUSE,ON_STOP,ON_DESTROY)
  • State 狀態 當前元件的生命週期狀態(INITIALIZED,DESTROYED,CREATED,STARTED,RESUMED)


LifecycleOwner

實現LifecycleOwner就表示這是個有生命週期的類,他有一個getLifecycle ()方法是必須實現的。com.android.support:appcompat-v7:26.1.0中的AppCompatActivity已經實現了這個介面,詳細的實現可以自行檢視程式碼。

對於前面提到的監聽位置的例子。可以把MyLocationListener實現LifecycleObserver,然後在LifecycleActivity/Fragment)的onCreate方法中初始化。這樣MyLocationListener就能自行處理生命週期帶來的問題。

class MyActivity extends AppCompatActivity {
   private MyLocationListener myLocationListener;

   public void onCreate(...) {
       myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
           // update UI
       });
       Util.checkUserStatus(result -> {
           if (result) {
               myLocationListener.enable();
           }
       });
 }
}

class MyLocationListener implements LifecycleObserver {
   private boolean enabled = false;
   public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
      ...
   }

   @OnLifecycleEvent(Lifecycle.Event.ON_START)
   void start() {
       if (enabled) {
          // connect
       }
   }

   public void enable() {
       enabled = true;
       if (lifecycle.getState().isAtLeast(STARTED)) {
           // connect if not connected
       }
   }

   @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
   void stop() {
       // disconnect if connected
   }
}

Lifecycles的最佳建議

  • 保持UI Controllers(Activity/Fragment)中程式碼足夠簡潔。一定不能包含如何獲取資料的程式碼,要通過ViewModel獲取LiveData形式的資料。
  • 用資料驅動UI,UI的職責就是根據資料改變顯示的內容,並且把使用者操作UI的行為傳遞給ViewModel。
  • 把業務邏輯相關的程式碼放到ViewModel中,把ViewModel看成是連結UI和App其他部分的膠水。但ViewModel不能直接獲取資料,要通過呼叫其他類來獲取資料。
  • 使用DataBinding來簡化View(佈局檔案)和UI Controllers(Activity/Fragment)之間的程式碼
  • 如果佈局本身太過複雜,可以考慮建立一個Presenter類來處理UI相關的改變。雖然這麼做會多寫很多程式碼,但是對於保持UI的簡介和可測試性是有幫助的。
  • 不要在ViewModel中持有任何View/Activity的context。否則會造成記憶體洩露。



LiveData

LiveData是一種持有可被觀察資料的類。和其他可被觀察的類不同的是,LiveData是有生命週期感知能力的,這意味著它可以在activities, fragments, 或者 services生命週期是活躍狀態時更新這些元件。

要想使用LiveData(或者這種有可被觀察資料能力的類)就必須配合實現了LifecycleOwner的物件使用。在這種情況下,當對應的生命週期物件DESTORY時,才能移除觀察者。這對Activity或者Fragment來說顯得尤為重要,因為他們可以在生命週期結束的時候立刻解除對資料的訂閱,從而避免記憶體洩漏等問題。

使用LiveData的優點

  • UI和實時資料保持一致 因為LiveData採用的是觀察者模式,這樣一來就可以在資料發生改變時獲得通知,更新UI。
  • 避免記憶體洩漏 觀察者被繫結到元件的生命週期上,當被繫結的元件銷燬(destory)時,觀察者會立刻自動清理自身的資料。
  • 不會再產生由於Activity處於stop狀態而引起的崩潰 例如:當Activity處於後臺狀態時,是不會收到LiveData的任何事件的。
  • 不需要再解決生命週期帶來的問題 LiveData可以感知被繫結的元件的生命週期,只有在活躍狀態才會通知資料變化。
  • 實時資料重新整理 當元件處於活躍狀態或者從不活躍狀態到活躍狀態時總是能收到最新的資料
  • 解決Configuration Change問題 在螢幕發生旋轉或者被回收再次啟動,立刻就能收到最新的資料。
  • 資料共享 如果對應的LiveData是單例的話,就能在app的元件間分享資料。這部分詳細的資訊可以參考繼承LiveData
  •  

使用LiveData

  1. 建立一個持有某種資料型別的LiveData (通常是在ViewModel中)
  2. 建立一個定義了onChange()方法的觀察者。這個方法是控制LiveData中資料發生變化時,採取什麼措施 (比如更新介面)。通常是在UI Controller (Activity/Fragment) 中建立這個觀察者。
  3. 通過 observe()方法連線觀察者和LiveData。observe()方法需要攜帶一個LifecycleOwner類。這樣就可以讓觀察者訂閱LiveData中的資料,實現實時更新。

 

建立LiveData物件

LiveData是一個數據的包裝。具體的包裝物件可以是任何資料,包括集合(比如List)。LiveData通常在ViewModel中建立,然後通過gatter方法獲取。具體可以看一下程式碼:

public class NameViewModel extends ViewModel {

// Create a LiveData with a String 暫時就把MutableLiveData看成是LiveData吧,下面的文章有詳細的解釋
private MutableLiveData<String> mCurrentName;

    public MutableLiveData<String> getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData<String>();
        }
        return mCurrentName;
    }

// Rest of the ViewModel...
}

觀察LiveData中的資料

通常情況下都是在元件的onCreate()方法中開始觀察資料,原因有以下兩點:

  • 系統會多次呼叫onResume()方法。
  • 確保Activity/Fragment在處於活躍狀態時立刻可以展示資料。

下面的程式碼展示瞭如何觀察LiveData物件:

public class NameActivity extends AppCompatActivity {

    private NameViewModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Other code to setup the activity...

        // Get the ViewModel.
        mModel = ViewModelProviders.of(this).get(NameViewModel.class);

        // Create the observer which updates the UI.
        final Observer<String> nameObserver = new Observer<String>() {
            @Override
            public void onChanged(@Nullable final String newName) {
                // Update the UI, in this case, a TextView.
                mNameTextView.setText(newName);
            }
        };

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        mModel.getCurrentName().observe(this, nameObserver);
    }
}

更新LiveData物件

如果想要在UI Controller中改變LiveData中的值呢?(比如點選某個Button把性別從男設定成女)。LiveData並沒有提供這樣的功能,但是Architecture Component提供了MutableLiveData這樣一個類,可以通過setValue(T)postValue(T)方法來修改儲存在LiveData中的資料。MutableLiveDataLiveData的一個子類,從名稱上也能看出這個類的作用。舉個直觀點的例子:

 

mButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String anotherName = "John Doe";
        mModel.getCurrentName().setValue(anotherName);
    }
});

呼叫setValue()方法就可以把LiveData中的值改為John Doe。同樣,通過這種方法修改LiveData中的值同樣會觸發所有對這個資料感興趣的類。那麼setValue()postValue()有什麼不同呢?區別就是setValue()只能在主執行緒中呼叫,而postValue()可以在子執行緒中呼叫。

Room和LiveData配合使用

Room可以返回LiveData的資料型別。這樣對資料庫中的任何改動都會被傳遞出去。這樣修改完資料庫就能獲取最新的資料,減少了主動獲取資料的程式碼。

   public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.(從資料庫中直接返回LiveData)
        return userDao.load(userId);
    }

 

繼承LiveData擴充套件功能

LiveData的活躍狀態包括:STARTED或者RESUMED兩種狀態。那麼如何在活躍狀態下把資料傳遞出去呢?

public class StockLiveData extends LiveData<BigDecimal> {
    private StockManager mStockManager;

    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };

    public StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }

    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }

    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}

可以看到onActive()onInactive()就表示了處於活躍和不活躍狀態的回撥。

public class MyFragment extends Fragment {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        LiveData<BigDecimal> myPriceListener = ...;
        myPriceListener.observe(this, price -> {
            // Update the UI.
        });
    }
}

如果把StockLiveData寫成單例模式,那麼還可以在不同的元件間共享資料。

public class StockLiveData extends LiveData<BigDecimal> {
    private static StockLiveData sInstance;
    private StockManager mStockManager;

    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };

    @MainThread
    public static StockLiveData get(String symbol) {
        if (sInstance == null) {
            sInstance = new StockLiveData(symbol);
        }
        return sInstance;
    }

    private StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }

    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }

    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}

轉換LiveData中的值(Transform LiveData)

這麼說很容易和上文改變LiveData中的值搞混。這裡的變換是指在LiveData的資料被分發到各個元件之前轉換值的內容,各個元件收到的是轉換後的值,但是LiveData裡面資料本身的值並沒有改變。(和RXJava中map的概念很像)Lifecycle包中提供了Transformations來提供轉換的功能。

Transformations.map()

LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});

把原來是包含User的LiveData轉換成包含String的LiveData傳遞出去。

Transformations.switchMap()

private LiveData<User> getUser(String id) {
  ...;
}

LiveData<String> userId = ...;
LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) );

和上面的map()方法很像。區別在於傳遞給switchMap()的函式必須返回LiveData物件。
和LiveData一樣,Transformation也可以在觀察者的整個生命週期中存在。只有在觀察者處於觀察LiveData狀態時,Transformation才會運算。Transformation是延遲運算的(calculated lazily),而生命週期感知的能力確保不會因為延遲發生任何問題。

如果在ViewModel物件的內部需要一個Lifecycle物件,那麼使用Transformation是一個不錯的方法。舉個例子:假如有個UI元件接受輸入的地址,返回對應的郵政編碼。那麼可以 實現一個ViewModel和這個元件繫結:

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    public MyViewModel(PostalCodeRepository repository) {
       this.repository = repository;
    }

    private LiveData<String> getPostalCode(String address) {
       // DON'T DO THIS (不要這麼幹)
       return repository.getPostCode(address);
    }
}

看程式碼中的註釋,有個// DON'T DO THIS (不要這麼幹),這是為什麼?有一種情況是如果UI元件被回收後又被重新建立,那麼又會觸發一次 repository.getPostCode(address)查詢,而不是重用上次已經獲取到的查詢。那麼應該怎樣避免這個問題呢?看一下下面的程式碼:

class MyViewModel extends ViewModel {
    private final PostalCodeRepository repository;
    private final MutableLiveData<String> addressInput = new MutableLiveData();
    public final LiveData<String> postalCode =
            Transformations.switchMap(addressInput, (address) -> {
                return repository.getPostCode(address);
             });

  public MyViewModel(PostalCodeRepository repository) {
      this.repository = repository
  }

  private void setInput(String address) {
      addressInput.setValue(address);
  }
}

合併多個LiveData中的資料

MediatorLiveData是LiveData的子類,可以通過MediatorLiveData合併多個LiveData來源的資料。同樣任意一個來源的LiveData資料發生變化,MediatorLiveData都會通知觀察他的物件。說的有點抽象,舉個例子。比如UI接收來自本地資料庫和網路資料,並更新相應的UI。可以把下面兩個LiveData加入到MeidatorLiveData中:

  • 關聯資料庫的LiveData
  • 關聯聯網請求的LiveData

相應的UI只需要關注MediatorLiveData就可以在任意資料來源更新時收到通知。

 

ViewModel

ViewModel設計的目的就是存放和處理和UI相關的資料,並且這些資料不受配置變化(Configuration Changes,例如:旋轉螢幕,元件被系統回收)的影響。

ViewModel用於為UI元件提供資料,並且能夠在旋轉螢幕等Configuration Change發生時,仍能保持裡面的資料。當UI元件恢復時,可以立刻向UI提供資料。

 

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // do async operation to fetch users
    }
}

Activity訪問User List資料:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

假如使用者按返回鍵,主動銷燬了這個Activity呢?這時系統會呼叫ViewModel的onCleared()方法,清除ViewModel中的資料。

 

在Fragments間分享資料

使用ViewModel可以很好的解決這個問題。假設有這樣兩個Fragment,一個Fragment提供一個列表,另一個Fragment提供點選每個item現實的詳細資訊。

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends LifecycleFragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // update UI
        });
    }
}

兩個Fragment都是通過getActivity()來獲取ViewModelProvider。這意味著兩個Activity都是獲取的屬於同一個Activity的同一個ShareViewModel例項。
這樣做優點如下:

  • Activity不需要寫任何額外的程式碼,也不需要關心Fragment之間的通訊。
  • Fragment不需要處理除SharedViewModel以外其他的程式碼。這兩個Fragment不需要知道對方是否存在。
  • Fragment的生命週期不會相互影響

ViewModel的生命週期

ViewModel只有在Activity finish或者Fragment detach之後才會銷燬。

 

Room

Room在SQLite上提供了一個方便訪問的抽象層。App把經常需要訪問的資料儲存在本地將會大大改善使用者的體驗。這樣使用者在網路不好時仍然可以瀏覽內容。當用戶網路可用時,可以更新使用者的資料。

使用原始的SQLite可以提供這樣的功能,但是有以下兩個缺點:

  • 沒有編譯時SQL語句的檢查。尤其是當你的資料庫表發生變化時,需要手動的更新相關程式碼,這會花費相當多的時間並且容易出錯。
  • 編寫大量SQL語句和Java物件之間相互轉化的程式碼。

Room包含以下三個重要組成部分:

 

 

User.java

@Entity
public class User {
    @PrimaryKey
    private int uid;

    @ColumnInfo(name = "first_name")
    private String firstName;

    @ColumnInfo(name = "last_name")
    private String lastName;

    // Getters and setters are ignored for brevity, 
    // but they're required for Room to work.
    //Getters和setters為了簡單起見就省略了,但是對Room來說是必須的
}

UserDao.java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

在建立了上面三個檔案後,就可以通過如下程式碼建立資料庫了:

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

Entities

@Entity
如果上面的User類中包含一個欄位是不希望存放到資料庫中的,那麼可以用@Ignore註解這個欄位:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    //不需要被存放到資料庫中
    @Ignore
    Bitmap picture;
}

Room持久化一個類的field必須要求這個field是可以訪問的。可以把這個field設為public或者設定setter和getter。

Primary Key 主鍵

每個Entity都必須定義一個field為主鍵,即使是這個Entity只有一個field。如果想要Room生成自動的primary key,可以使用@PrimaryKeyautoGenerate屬性。如果Entity的primary key是多個Field的複合Key,可以向下面這樣設定:

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

在預設情況下Room使用類名作為資料庫表的名稱。如果想要設定不同的名稱,可以參考下面的程式碼,設定表名tableName為users:

@Entity(tableName = "users")
class User {
    ...
}

和設定tableName相似,Room預設使用field的名稱作為表的列名。如果想要使用不同的名稱,可以通過@ColumnInfo(name = "first_name")設定,程式碼如下:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

和設定tableName相似,Room預設使用field的名稱作為表的列名。如果想要使用不同的名稱,可以通過@ColumnInfo(name = "first_name")設定,程式碼如下:

@Entity(tableName = "users")
class User {
    @PrimaryKey
    public int id;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;

    @Ignore
    Bitmap picture;
}

索引和唯一性

外來鍵

例如,有一個Pet類需要和User類建立關係,可以通過@ForeignKey來達到這個目的,程式碼如下:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Pet {
    @PrimaryKey
    public int petId;

    public String name;

    @ColumnInfo(name = "user_id")
    public int userId;
}

物件巢狀物件

class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

 

Data Access Objects(DAOs)

DAOs是資料庫訪問的抽象層。
Dao可以是一個介面也可以是一個抽象類。如果是抽象類,那麼它可以接受一個RoomDatabase作為構造器的唯一引數。
Room不允許在主執行緒中訪問資料庫,除非在builder裡面呼叫allowMainThreadQueries() 。因為訪問資料庫是耗時的,可能阻塞主執行緒,引起UI卡頓。

 

新增方便使用的方法

Insert

使用 @Insert註解的方法,Room將會生成插入的程式碼。

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

如果@Insert 方法只接受一個引數,那麼將返回一個long,對應著插入的rowId。如果接受多個引數,或者陣列,或者集合,那麼就會返回一個long的陣列或者list。

Update

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

也可以讓update方法返回一個int型的整數,代表被update的行號。

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

 

Paging Library

Paging Library(分頁載入庫)用於逐步從資料來源載入資訊,而不會耗費過多的裝置資源或者等待太長的時間。

總體概覽

一個常見的需求是獲取很多資料,但是同時也只展示其中的一小部分資料。這就會引起很多問題,比如浪費資源和流量等。
現有的Android APIs可以提供分頁載入的功能,但是也帶來了顯著的限制和缺點:

  • CursorAdapter,使得從資料庫載入資料到ListView變得非常容易。但是這是在主執行緒中查詢資料庫,並且分頁的內容使用低效的 Cursor返回。更多使用CursorAdapter帶來的問題參考Large Database Queries on Android
  • AsyncListUtil提供基於位置的( position-based)分頁載入到 RecyclerView中,但是無法使用不基於位置(non-positional)的分頁載入,而且還強制把null作為佔位符。

提供的類

DataSource
資料來源。根據你想要訪問資料的方式,可以有兩種子類可供選擇:

例如使用 Room persistence library 就可以自動建立返回 TiledDataSource型別的資料:

@Query("select * from users WHERE age > :age order by name DESC, id ASC")
TiledDataSource<User> usersOlderThan(int age);

 

@Dao
interface UserDao {
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract LivePagedListProvider<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = userDao.usersByLastName().create(
                /* initial load position */ 0,
                new PagedList.Config.Builder()
                        .setPageSize(50)
                        .setPrefetchDistance(50)
                        .build());
    }
}

class MyActivity extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        UserAdapter<User> adapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
        recyclerView.setAdapter(adapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}