NO IMAGE

淺談Android Architecture Components


簡介

Google IO 2017釋出Android Architecture Components,自己先研究了一下,蠻有意思的,特來記錄一下。本文內容主要是參考官方文件以及自己的理解,如有錯誤之處,懇請指出。

Android Architecture Components

Android Architecture Components是Google釋出的一套新的架構元件,使App的架構更加健壯,後面簡稱AAC。

AAC主要提供了Lifecycle,ViewModel,LiveData,Room等功能,下面依次說明:

  • Lifecycle

生命週期管理,把原先Android生命週期的中的程式碼抽取出來,如將原先需要在onStart()等生命週期中執行的程式碼分離到Activity或者Fragment之外。

  • LiveData

一個資料持有類,持有資料並且這個資料可以被觀察被監聽,和其他Observer不同的是,它是和Lifecycle是繫結的,在生命週期內使用有效,減少記憶體洩露和引用問題。

  • ViewModel

用於實現架構中的ViewModel,同時是與Lifecycle繫結的,使用者無需擔心生命週期。可以在多個Fragment之間共享資料,比如旋轉螢幕後Activity會重新create,這時候使用ViewModel還是之前的資料,不需要再次請求網路資料。

  • Room

谷歌推出的一個Sqlite ORM庫,不過使用起來還不錯,使用註解,極大簡化資料庫的操作,有點類似Retrofit的風格。

AAC的架構是這樣的:

新的架構

  • Activity/Fragment

UI層,通常是Activity/Fragment等,監聽ViewModel,當VIewModel資料更新時重新整理UI,監聽使用者事件反饋到ViewModel,主流的資料驅動介面。

  • ViewModel

持有或儲存資料,向Repository中獲取資料,響應UI層的事件,執行響應的操作,響應資料變化並通知到UI層。

  • Repository

App的完全的資料模型,ViewModel互動的物件,提供簡單的資料修改和獲取的介面,配合好網路層資料的更新與本地持久化資料的更新,同步等

  • Data Source

包含本地的資料庫等,網路api等,這些基本上和現有的一些MVVM,以及Clean架構的組合比較相似

Gradle 整合

根目錄gradle檔案中新增Google Maven Repository

allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}

在模組中新增對應的依賴

如使用Lifecycle,LiveData、ViewModel,新增如下依賴。

compile "android.arch.lifecycle:runtime:1.0.0-alpha1"
compile "android.arch.lifecycle:extensions:1.0.0-alpha1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha1"

如使用Room功能,新增如下依賴。

compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"
// For testing Room migrations, add:
testCompile "android.arch.persistence.room:testing:1.0.0-alpha1"
// For Room RxJava support, add:
compile "android.arch.persistence.room:rxjava2:1.0.0-alpha1"

LifeCycles

Android開發中,經常需要管理生命週期。舉個栗子,我們需要獲取使用者的地址位置,當這個Activity在顯示的時候,我們開啟定位功能,然後實時獲取到定位資訊,當頁面被銷燬的時候,需要關閉定位功能。

下面是簡單的示例程式碼。

class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// update UI
});
}
public void onStart() {
super.onStart();
myLocationListener.start();
}
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}

上面只是一個簡單的場景,我們來個複雜一點的場景。當定位功能需要滿足一些條件下才開啟,那麼會變得複雜多了。可能在執行Activity的stop方法時,定位的start方法才剛剛開始執行,比如如下程式碼,這樣生命週期管理就變得很麻煩了。

class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, location -> {
// update UI
});
}
public void onStart() {
super.onStart();
Util.checkUserStatus(result -> {
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start();
}
});
}
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}

AAC中提供了Lifecycle,用來幫助我們解決這樣的問題。LifeCycle使用2個列舉類來解決生命週期管理問題。一個是事件,一個是狀態。

事件:

生命週期事件由系統來分發,這些事件對於與Activity和Fragment的生命週期函式。

狀態:

Lifecycle的狀態,用於追蹤中Lifecycle物件,如下圖所示。

Lifecycle的狀態

上面的定位功能程式碼,使用LifeCycle實現以後是這樣的,實現一個LifecycleObserver介面,然後用註解標註狀態,最後在LifecycleOwner中新增監聽。

public class MyObserver implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause() {
}
}
aLifecycleOwner.getLifecycle().addObserver(new MyObserver());

上面程式碼中用到了aLifecycleOwnerLifecycleOwner介面物件,LifecycleOwner是一個只有一個方法的介面getLifecycle(),需要由子類來實現。

在Lib中已經有實現好的子類,我們可以直接拿來使用。比如LifecycleActivityLifecycleFragment,我們只需要繼承此類就行了。

當然實際開發的時候我們會自己定義BaseActivity,Java是單繼承模式,那麼需要自己實現LifecycleRegistryOwner介面。

如下所示即可,程式碼很近簡單

public class BaseFragment extends Fragment implements LifecycleRegistryOwner {
LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
@Override
public LifecycleRegistry getLifecycle() {
return lifecycleRegistry;
}
}

LiveData

LiveData 是一個 Data Holder 類,可以持有資料,同時這個資料可以被監聽的,當資料改變的時候,可以觸發回撥。但是又不像普通的Observable,LiveData繫結了App的元件,LiveData可以指定在LifeCycle的某個狀態被觸發。比如LiveData可以指定在LifeCycle的 STARTED 或 RESUMED狀體被觸發。

public class LocationLiveData extends LiveData<Location> {
private LocationManager locationManager;
private SimpleLocationListener listener = new SimpleLocationListener() {
@Override
public void onLocationChanged(Location location) {
setValue(location);
}
};
public LocationLiveData(Context context) {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
protected void onActive() {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
}
@Override
protected void onInactive() {
locationManager.removeUpdates(listener);
}
}
  • onActive()

這個方法在LiveData在被啟用的狀態下執行,我們可以開始執行一些操作。

  • onActive()

這個方法在LiveData在的失去活性狀態下執行,我們可以結束執行一些操作。

  • setValue()

執行這個方法的時候,LiveData可以觸發它的回撥。

LocationLiveData可以這樣使用。

public class MyFragment extends LifecycleFragment {
public void onActivityCreated (Bundle savedInstanceState) {
LiveData<Location> myLocationListener = ...;
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.addObserver(this, location -> {
// update UI
});
}
});
}
}

注意,上面的addObserver方法,必須傳LifecycleOwner物件,也就是說新增的物件必須是可以被LifeCycle管理的。

如果LifeCycle沒有觸發對對應的狀態(STARTED or RESUMED),它的值被改變了,那麼Observe就不會被執行,

如果LifeCycle被銷燬了,那麼Observe將自動被刪除。

實際上LiveData就提供一種新的供資料共享方式。可以用它在多個Activity、Fragment等其他有生命週期管理的類中實現資料共享。

還是上面的定位例子。

public class LocationLiveData extends LiveData<Location> {
private static LocationLiveData sInstance;
private LocationManager locationManager;
@MainThread
public static LocationLiveData get(Context context) {
if (sInstance == null) {
sInstance = new LocationLiveData(context.getApplicationContext());
}
return sInstance;
}
private SimpleLocationListener listener = new SimpleLocationListener() {
@Override
public void onLocationChanged(Location location) {
setValue(location);
}
};
private LocationLiveData(Context context) {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
protected void onActive() {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
}
@Override
protected void onInactive() {
locationManager.removeUpdates(listener);
}
}

然後在Fragment中呼叫。

public class MyFragment extends LifecycleFragment {
public void onActivityCreated (Bundle savedInstanceState) {
Util.checkUserStatus(result -> {
if (result) {
LocationLiveData.get(getActivity()).observe(this, location -> {
// update UI
});
}
});
}
}

從上面的示例,可以得到使用LiveData優點:

  • 沒有記憶體洩露的風險,全部繫結到對應的生命週期,當LifeCycle被銷燬的時候,它們也自動被移除

  • 降低Crash,當Activity被銷燬的時候,LiveData的Observer自動被刪除,然後UI就不會再接受到通知

  • 實時資料,因為LiveData是持有真正的資料的,所以當生命週期又重新開始的時候,又可以重新拿到資料

  • 正常配置改變,當Activity或者Fragment重新建立的時候,可以從LiveData中獲取上一次有用的資料

  • 不再需要手動的管理生命週期

Transformations

有時候需要對一個LiveData做Observer,但是這個LiveData是依賴另外一個LiveData,有點類似於RxJava中的操作符,我們可以這樣做。

  • Transformations.map()

用於事件流的傳遞,用於觸發下游資料。

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

這個和map類似,只不過這個是用來觸發上游資料。

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

ViewModel

ViewModel是用來儲存UI層的資料,以及管理對應的資料,當資料修改的時候,可以馬上重新整理UI。

Android系統提供控制元件,比如Activity和Fragment,這些控制元件都是具有生命週期方法,這些生命週期方法被系統呼叫。

當這些控制元件被銷燬或者被重建的時候,如果資料儲存在這些物件中,那麼資料就會丟失。比如在一個介面,儲存了一些使用者資訊,當介面重新建立的時候,就需要重新去獲取資料。當然了也可以使用控制元件自動再帶的方法,在onSaveInstanceState方法中儲存資料,在onCreate中重新獲得資料,但這僅僅在資料量比較小的情況下。如果資料量很大,這種方法就不能適用了。

另外一個問題就是,經常需要在Activity中載入資料,這些資料可能是非同步的,因為獲取資料需要花費很長的時間。那麼Activity就需要管理這些資料呼叫,否則很有可能會產生記憶體洩露問題。最後需要做很多額外的操作,來保證程式的正常執行。

同時Activity不僅僅只是用來載入資料的,還要載入其他資源,做其他的操作,最後Activity類變大,就是我們常講的上帝類。也有不少架構是把一些操作放到單獨的類中,比如MVP就是這樣,建立相同類似於生命週期的函式做代理,這樣可以減少Activity的程式碼量,但是這樣就會變得很複雜,同時也難以測試。

AAC中提供ViewModel可以很方便的用來管理資料。我們可以利用它來管理UI元件與資料的繫結關係。ViewModel提供自動繫結的形式,當資料來源有更新的時候,可以自動立即的更新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
}
}
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
});
}
}

當我們獲取ViewModel例項的時候,ViewModel是通過ViewModelProvider儲存在LifeCycle中,ViewModel會一直儲存在LifeCycle中,直到Activity或者Fragment銷燬了,觸發LifeCycle被銷燬,那麼ViewModel也會被銷燬的。下面是ViewModel的生命週期圖。

Room

Room是一個持久化工具,和ormlite、greenDao類似,都是ORM工具。在開發中我們可以利用Room來操作sqlite資料庫。

Room主要分為三個部分:

  • Database

使用註解申明一個類,註解中包含若干個Entity類,這個Database類主要負責建立資料庫以及獲取資料物件的。

  • Entity

表示每個資料庫的總的一個表結構,同樣也是使用註解表示,類中的每個欄位都對應表中的一列。

  • DAO

DAO是 Data Access Object的縮寫,表示從從程式碼中直接訪問資料庫,遮蔽sql語句。

下面是官方給的結構圖。

程式碼示例:

// 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.
}
// 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();
}

最後在程式碼中呼叫Database物件

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

注意: Database最好設計成單利模式,否則物件太多會有效能的影響。

Entities

@Entity
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}

@Entity用來註解一個實體類,對應資料庫一張表。預設情況下,Room為實體中定義的每個成員變數在資料中建立對應的欄位,我們可能不想儲存到資料庫的欄位,這時候就要用道@Ignore註解。

注意: 為了儲存每一個欄位,這個欄位需要有可以訪問的gettter/setter方法或者是public的屬性

Entity的引數 primaryKeys

每個實體必須至少定義1個欄位作為主鍵,即使只有一個成員變數,除了使用@PrimaryKey 將欄位標記為主鍵的方式之外,還可以通過在@Entity註解中指定引數的形式

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

Entity的引數 tableName

預設情況下,Room使用類名作為資料庫表名。如果你想表都有一個不同的名稱,就可以在@Entity中使用tableName引數指定

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

和tableName作用類似; @ColumnInfo註解是改變成員變數對應的資料庫的欄位名稱。

Entity的引數 indices

indices的引數值是@Index的陣列,在某些情況寫為了加快查詢速度我們可以需要加入索引

@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}

有時,資料庫中某些欄位或欄位組必須是唯一的。通過將@Index的unique 設定為true,可以強制執行此唯一性屬性。

下面的程式碼示例防止表有兩行包含FirstName和LastName列值相同的一組:

@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)})
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}

Entity的引數 foreignKeys

因為SQLite是一個關係型資料庫,你可以指定物件之間的關係。儘管大多數ORM庫允許實體物件相互引用,但Room明確禁止。實體之間沒有物件引用。

不能使用直接關係,所以就要用到foreignKeys(外來鍵)。

@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}

外來鍵是非常強大的,因為它允許指定引用實體更新時發生的操作。例如,級聯刪除,你可以告訴SQLite刪除所有書籍的使用者如果使用者對應的例項是由包括OnDelete =CASCADE在@ForeignKey註釋。ON_CONFLICT : @Insert(onConflict=REPLACE) REMOVE 或者 REPLACE

有時候可能還需要物件巢狀這時候可以用@Embedded註解

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;
}

Dao

@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);
}

資料訪問物件Data Access Objects (DAOs)是一種抽象訪問資料庫的乾淨的方式。

DAO的Insert 操作

當建立DAO方法並用@Insert註釋它時,生成一個實現時會在一個事務中完成插入所有引數到資料庫。

@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);
}

DAO的Update、Delete操作

與上面類似

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

DAO的Query 操作

一個簡單查詢示例

@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}

稍微複雜的,帶引數的查詢操作

@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}

也可以帶多個引數

@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}

返回子集

上面示例都是查詢一個表中的所有欄位,結果用對應的Entity即可,但是如果我只要其中的幾個欄位,那麼該怎麼使用呢?

比如上面的User,我只需要firstName和lastName,首先定義一個子集,然後結果改成對應子集即可。

public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}

支援集合引數

有個這樣一個查詢需求,比如要查詢某兩個地區的所有使用者,直接用sql中的in即可,但是如果這個地區是程式指定的,個數不確定,那麼改怎麼辦?

@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

支援Observable

前面提到了LiveData,可以非同步的獲取資料,那麼我們的Room也是支援非同步查詢的。

@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}

支援RxJava

RxJava是另外一個非同步操作庫,同樣也是支援的。

@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}

支援Cursor

原始的Android系統查詢結果是通過Cursor來獲取的,同樣也支援。

@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}

多表查詢

有時候資料庫存在正規化相關,資料拆到了多個表中,那麼就需要關聯多個表進行查詢,如果結果只是一個表的資料,那麼很簡單,直接用Entity定義的型別即可。

@Dao
public interface MyDao {
@Query("SELECT * FROM book "
"INNER JOIN loan ON loan.book_id = book.id "
"INNER JOIN user ON user.id = loan.user_id "
"WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}

如果結果是部分欄位,同上面一樣,需要單獨定義一個POJO,來接受資料。

public class UserPet {
public String userName;
public String petName;
}
@Dao
public interface MyDao {
@Query("SELECT user.name AS userName, pet.name AS petName "
"FROM user, pet "
"WHERE user.id = pet.user_id")
public LiveData<List<UserPet>> loadUserAndPetNames();
}

型別轉換

有時候Java定義的資料型別和資料庫中儲存的資料型別是不一樣的,Room提供型別轉換,這樣在運算元據庫的時候,可以自動轉換。

比如在Java中,時間用Date表示,但是在資料庫中型別確實long,這樣有利於儲存。

public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}

定義資料庫時候需要指定型別轉換,同時定義好Entity和Dao。

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
@Entity
public class User {
...
private Date birthday;
}
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}

資料庫升級

版本迭代中,我們不可避免的會遇到資料庫升級問題,Room也為我們提供了資料庫升級的處理介面。

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER");
}
};

遷移過程結束後,Room將驗證架構以確保遷移正確發生。如果Room發現問題,則丟擲包含不匹配資訊的異常。

警告: 如果不提供必要的遷移,Room會重新構建資料庫,這意味著將丟失資料庫中的所有資料。

輸出模式

可以在gradle中設定開啟輸出模式,便於我們除錯,檢視資料庫表情況,以及做資料庫遷移。

android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}

Sample

這裡是官方示例,本人自己參考官方文件和示例Android Architecture Components samples後,也寫出了一個類似的示例專案XiaoxiaZhihu_AAC,還請多多指教。

總結

原先IO是在5月底已經結束,本來想盡快參照官方文件和示例,把API擼一遍,然後寫個Demo和文章來介紹一下。程式碼示例早已經寫出來了,但6月分工作太忙,然後又出差到北京,最後等到了6月底了,才把這篇文章給寫出來了。中間可能有內容以及改變,如果有發現穩重有錯誤,請及時指出,不理賜教。同時以後做事一定要有始有終,確定的事一定要堅持。

相關資料

Android Architecture Components

簡單聊聊Android Architecture Componets

Android Architecture Components samples

XiaoxiaZhihu_AAC

淺談Android Architecture Components

Room ORM 資料庫框架