淺談Android Architecture Components

淺談Android Architecture Components


簡(jiǎn)介

Google IO 2017發(fā)布Android Architecture Components拇舀,自己先研究了一下逻族,蠻有意思的,特來(lái)記錄一下骄崩。本文內(nèi)容主要是參考官方文檔以及自己的理解聘鳞,如有錯(cuò)誤之處,懇請(qǐng)指出要拂。

Android Architecture Components

Android Architecture Components是Google發(fā)布的一套新的架構(gòu)組件抠璃,使App的架構(gòu)更加健壯,后面簡(jiǎn)稱AAC脱惰。

AAC主要提供了Lifecycle搏嗡,ViewModel,LiveData枪芒,Room等功能彻况,下面依次說(shuō)明:

  • Lifecycle

生命周期管理谁尸,把原先Android生命周期的中的代碼抽取出來(lái),如將原先需要在onStart()等生命周期中執(zhí)行的代碼分離到Activity或者Fragment之外纽甘。

  • LiveData

一個(gè)數(shù)據(jù)持有類良蛮,持有數(shù)據(jù)并且這個(gè)數(shù)據(jù)可以被觀察被監(jiān)聽,和其他Observer不同的是悍赢,它是和Lifecycle是綁定的决瞳,在生命周期內(nèi)使用有效,減少內(nèi)存泄露和引用問(wèn)題左权。

  • ViewModel

用于實(shí)現(xiàn)架構(gòu)中的ViewModel皮胡,同時(shí)是與Lifecycle綁定的,使用者無(wú)需擔(dān)心生命周期赏迟÷藕兀可以在多個(gè)Fragment之間共享數(shù)據(jù),比如旋轉(zhuǎn)屏幕后Activity會(huì)重新create锌杀,這時(shí)候使用ViewModel還是之前的數(shù)據(jù)甩栈,不需要再次請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)。

  • Room

谷歌推出的一個(gè)Sqlite ORM庫(kù)糕再,不過(guò)使用起來(lái)還不錯(cuò)量没,使用注解,極大簡(jiǎn)化數(shù)據(jù)庫(kù)的操作突想,有點(diǎn)類似Retrofit的風(fēng)格殴蹄。

AAC的架構(gòu)是這樣的:

新的架構(gòu)
  • Activity/Fragment

UI層,通常是Activity/Fragment等猾担,監(jiān)聽ViewModel袭灯,當(dāng)VIewModel數(shù)據(jù)更新時(shí)刷新UI,監(jiān)聽用戶事件反饋到ViewModel垒探,主流的數(shù)據(jù)驅(qū)動(dòng)界面妓蛮。

  • ViewModel

持有或保存數(shù)據(jù),向Repository中獲取數(shù)據(jù)圾叼,響應(yīng)UI層的事件蛤克,執(zhí)行響應(yīng)的操作,響應(yīng)數(shù)據(jù)變化并通知到UI層夷蚊。

  • Repository

App的完全的數(shù)據(jù)模型构挤,ViewModel交互的對(duì)象,提供簡(jiǎn)單的數(shù)據(jù)修改和獲取的接口惕鼓,配合好網(wǎng)絡(luò)層數(shù)據(jù)的更新與本地持久化數(shù)據(jù)的更新筋现,同步等

  • Data Source

包含本地的數(shù)據(jù)庫(kù)等,網(wǎng)絡(luò)api等,這些基本上和現(xiàn)有的一些MVVM矾飞,以及Clean架構(gòu)的組合比較相似

Gradle 集成

根目錄gradle文件中添加Google Maven Repository

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

在模塊中添加對(duì)應(yīng)的依賴

如使用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開發(fā)中申眼,經(jīng)常需要管理生命周期瞒津。舉個(gè)栗子,我們需要獲取用戶的地址位置括尸,當(dāng)這個(gè)Activity在顯示的時(shí)候巷蚪,我們開啟定位功能,然后實(shí)時(shí)獲取到定位信息濒翻,當(dāng)頁(yè)面被銷毀的時(shí)候屁柏,需要關(guān)閉定位功能。

下面是簡(jiǎn)單的示例代碼有送。

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

上面只是一個(gè)簡(jiǎn)單的場(chǎng)景前联,我們來(lái)個(gè)復(fù)雜一點(diǎn)的場(chǎng)景。當(dāng)定位功能需要滿足一些條件下才開啟娶眷,那么會(huì)變得復(fù)雜多了⌒ネ危可能在執(zhí)行Activity的stop方法時(shí)届宠,定位的start方法才剛剛開始執(zhí)行,比如如下代碼乘粒,這樣生命周期管理就變得很麻煩了豌注。

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,用來(lái)幫助我們解決這樣的問(wèn)題灯萍。LifeCycle使用2個(gè)枚舉類來(lái)解決生命周期管理問(wèn)題轧铁。一個(gè)是事件,一個(gè)是狀態(tài)旦棉。

事件:

生命周期事件由系統(tǒng)來(lái)分發(fā)齿风,這些事件對(duì)于與Activity和Fragment的生命周期函數(shù)。

狀態(tài):

Lifecycle的狀態(tài)绑洛,用于追蹤中Lifecycle對(duì)象救斑,如下圖所示。

Lifecycle的狀態(tài)

上面的定位功能代碼真屯,使用LifeCycle實(shí)現(xiàn)以后是這樣的脸候,實(shí)現(xiàn)一個(gè)LifecycleObserver接口,然后用注解標(biāo)注狀態(tài),最后在LifecycleOwner中添加監(jiān)聽运沦。

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接口對(duì)象泵额,LifecycleOwner是一個(gè)只有一個(gè)方法的接口getLifecycle(),需要由子類來(lái)實(shí)現(xiàn)携添。

在Lib中已經(jīng)有實(shí)現(xiàn)好的子類嫁盲,我們可以直接拿來(lái)使用。比如LifecycleActivityLifecycleFragment薪寓,我們只需要繼承此類就行了亡资。

當(dāng)然實(shí)際開發(fā)的時(shí)候我們會(huì)自己定義BaseActivity,Java是單繼承模式向叉,那么需要自己實(shí)現(xiàn)LifecycleRegistryOwner接口锥腻。

如下所示即可,代碼很近簡(jiǎn)單

public class BaseFragment extends Fragment implements LifecycleRegistryOwner {

    LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);

    @Override
    public LifecycleRegistry getLifecycle() {
        return lifecycleRegistry;
    }
}

LiveData

LiveData 是一個(gè) Data Holder 類母谎,可以持有數(shù)據(jù)瘦黑,同時(shí)這個(gè)數(shù)據(jù)可以被監(jiān)聽的,當(dāng)數(shù)據(jù)改變的時(shí)候奇唤,可以觸發(fā)回調(diào)幸斥。但是又不像普通的Observable,LiveData綁定了App的組件咬扇,LiveData可以指定在LifeCycle的某個(gè)狀態(tài)被觸發(fā)甲葬。比如LiveData可以指定在LifeCycle的 STARTED 或 RESUMED狀體被觸發(fā)。

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()

這個(gè)方法在LiveData在被激活的狀態(tài)下執(zhí)行懈贺,我們可以開始執(zhí)行一些操作经窖。

  • onActive()

這個(gè)方法在LiveData在的失去活性狀態(tài)下執(zhí)行,我們可以結(jié)束執(zhí)行一些操作梭灿。

  • setValue()

執(zhí)行這個(gè)方法的時(shí)候画侣,LiveData可以觸發(fā)它的回調(diào)。

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對(duì)象皮迟,也就是說(shuō)添加的對(duì)象必須是可以被LifeCycle管理的搬泥。

如果LifeCycle沒(méi)有觸發(fā)對(duì)對(duì)應(yīng)的狀態(tài)(STARTED or RESUMED),它的值被改變了万栅,那么Observe就不會(huì)被執(zhí)行佑钾,

如果LifeCycle被銷毀了,那么Observe將自動(dòng)被刪除烦粒。

實(shí)際上LiveData就提供一種新的供數(shù)據(jù)共享方式休溶〈蓿可以用它在多個(gè)Activity、Fragment等其他有生命周期管理的類中實(shí)現(xiàn)數(shù)據(jù)共享兽掰。

還是上面的定位例子芭碍。

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中調(diào)用。

public class MyFragment extends LifecycleFragment {

    public void onActivityCreated (Bundle savedInstanceState) {

        Util.checkUserStatus(result -> {
            if (result) {
                LocationLiveData.get(getActivity()).observe(this, location -> {
                   // update UI
                });
            }
        });
  }
}

從上面的示例孽尽,可以得到使用LiveData優(yōu)點(diǎn):

  • 沒(méi)有內(nèi)存泄露的風(fēng)險(xiǎn)窖壕,全部綁定到對(duì)應(yīng)的生命周期,當(dāng)LifeCycle被銷毀的時(shí)候杉女,它們也自動(dòng)被移除

  • 降低Crash瞻讽,當(dāng)Activity被銷毀的時(shí)候,LiveData的Observer自動(dòng)被刪除熏挎,然后UI就不會(huì)再接受到通知

  • 實(shí)時(shí)數(shù)據(jù)速勇,因?yàn)長(zhǎng)iveData是持有真正的數(shù)據(jù)的,所以當(dāng)生命周期又重新開始的時(shí)候坎拐,又可以重新拿到數(shù)據(jù)

  • 正常配置改變烦磁,當(dāng)Activity或者Fragment重新創(chuàng)建的時(shí)候,可以從LiveData中獲取上一次有用的數(shù)據(jù)

  • 不再需要手動(dòng)的管理生命周期

Transformations

有時(shí)候需要對(duì)一個(gè)LiveData做Observer哼勇,但是這個(gè)LiveData是依賴另外一個(gè)LiveData都伪,有點(diǎn)類似于RxJava中的操作符,我們可以這樣做积担。

  • Transformations.map()

用于事件流的傳遞陨晶,用于觸發(fā)下游數(shù)據(jù)。

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

這個(gè)和map類似帝璧,只不過(guò)這個(gè)是用來(lái)觸發(fā)上游數(shù)據(jù)珍逸。

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

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

ViewModel

ViewModel是用來(lái)存儲(chǔ)UI層的數(shù)據(jù),以及管理對(duì)應(yīng)的數(shù)據(jù)聋溜,當(dāng)數(shù)據(jù)修改的時(shí)候,可以馬上刷新UI叭爱。

Android系統(tǒng)提供控件撮躁,比如Activity和Fragment,這些控件都是具有生命周期方法买雾,這些生命周期方法被系統(tǒng)調(diào)用把曼。

當(dāng)這些控件被銷毀或者被重建的時(shí)候,如果數(shù)據(jù)保存在這些對(duì)象中漓穿,那么數(shù)據(jù)就會(huì)丟失嗤军。比如在一個(gè)界面,保存了一些用戶信息晃危,當(dāng)界面重新創(chuàng)建的時(shí)候叙赚,就需要重新去獲取數(shù)據(jù)老客。當(dāng)然了也可以使用控件自動(dòng)再帶的方法,在onSaveInstanceState方法中保存數(shù)據(jù)震叮,在onCreate中重新獲得數(shù)據(jù)胧砰,但這僅僅在數(shù)據(jù)量比較小的情況下。如果數(shù)據(jù)量很大苇瓣,這種方法就不能適用了尉间。

另外一個(gè)問(wèn)題就是,經(jīng)常需要在Activity中加載數(shù)據(jù)击罪,這些數(shù)據(jù)可能是異步的哲嘲,因?yàn)楂@取數(shù)據(jù)需要花費(fèi)很長(zhǎng)的時(shí)間。那么Activity就需要管理這些數(shù)據(jù)調(diào)用媳禁,否則很有可能會(huì)產(chǎn)生內(nèi)存泄露問(wèn)題眠副。最后需要做很多額外的操作,來(lái)保證程序的正常運(yùn)行损话。

同時(shí)Activity不僅僅只是用來(lái)加載數(shù)據(jù)的侦啸,還要加載其他資源,做其他的操作丧枪,最后Activity類變大光涂,就是我們常講的上帝類。也有不少架構(gòu)是把一些操作放到單獨(dú)的類中拧烦,比如MVP就是這樣忘闻,創(chuàng)建相同類似于生命周期的函數(shù)做代理,這樣可以減少Activity的代碼量恋博,但是這樣就會(huì)變得很復(fù)雜齐佳,同時(shí)也難以測(cè)試。

AAC中提供ViewModel可以很方便的用來(lái)管理數(shù)據(jù)债沮。我們可以利用它來(lái)管理UI組件與數(shù)據(jù)的綁定關(guān)系炼吴。ViewModel提供自動(dòng)綁定的形式,當(dāng)數(shù)據(jù)源有更新的時(shí)候疫衩,可以自動(dòng)立即的更新UI硅蹦。

下面是一個(gè)簡(jiǎn)單的代碼示例。

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

當(dāng)我們獲取ViewModel實(shí)例的時(shí)候,ViewModel是通過(guò)ViewModelProvider保存在LifeCycle中,ViewModel會(huì)一直保存在LifeCycle中岳颇,直到Activity或者Fragment銷毀了粟矿,觸發(fā)LifeCycle被銷毀,那么ViewModel也會(huì)被銷毀的。下面是ViewModel的生命周期圖。

Room

Room是一個(gè)持久化工具,和ormlite生音、greenDao類似宁否,都是ORM工具。在開發(fā)中我們可以利用Room來(lái)操作sqlite數(shù)據(jù)庫(kù)久锥。

Room主要分為三個(gè)部分:

  • Database

使用注解申明一個(gè)類家淤,注解中包含若干個(gè)Entity類,這個(gè)Database類主要負(fù)責(zé)創(chuàng)建數(shù)據(jù)庫(kù)以及獲取數(shù)據(jù)對(duì)象的瑟由。

  • Entity

表示每個(gè)數(shù)據(jù)庫(kù)的總的一個(gè)表結(jié)構(gòu)絮重,同樣也是使用注解表示,類中的每個(gè)字段都對(duì)應(yīng)表中的一列歹苦。

  • DAO

DAO是 Data Access Object的縮寫青伤,表示從從代碼中直接訪問(wèn)數(shù)據(jù)庫(kù),屏蔽sql語(yǔ)句殴瘦。

下面是官方給的結(jié)構(gòu)圖狠角。

代碼示例:

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

最后在代碼中調(diào)用Database對(duì)象

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

注意: Database最好設(shè)計(jì)成單利模式,否則對(duì)象太多會(huì)有性能的影響蚪腋。

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用來(lái)注解一個(gè)實(shí)體類丰歌,對(duì)應(yīng)數(shù)據(jù)庫(kù)一張表。默認(rèn)情況下屉凯,Room為實(shí)體中定義的每個(gè)成員變量在數(shù)據(jù)中創(chuàng)建對(duì)應(yīng)的字段立帖,我們可能不想保存到數(shù)據(jù)庫(kù)的字段,這時(shí)候就要用道@Ignore注解悠砚。

注意: 為了保存每一個(gè)字段晓勇,這個(gè)字段需要有可以訪問(wèn)的gettter/setter方法或者是public的屬性

Entity的參數(shù) primaryKeys

每個(gè)實(shí)體必須至少定義1個(gè)字段作為主鍵,即使只有一個(gè)成員變量灌旧,除了使用@PrimaryKey 將字段標(biāo)記為主鍵的方式之外绑咱,還可以通過(guò)在@Entity注解中指定參數(shù)的形式

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

    @Ignore
    Bitmap picture;
}

Entity的參數(shù) tableName

默認(rèn)情況下,Room使用類名作為數(shù)據(jù)庫(kù)表名枢泰。如果你想表都有一個(gè)不同的名稱描融,就可以在@Entity中使用tableName參數(shù)指定

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

和tableName作用類似; @ColumnInfo注解是改變成員變量對(duì)應(yīng)的數(shù)據(jù)庫(kù)的字段名稱衡蚂。

Entity的參數(shù) indices

indices的參數(shù)值是@Index的數(shù)組稼稿,在某些情況寫為了加快查詢速度我們可以需要加入索引

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

有時(shí),數(shù)據(jù)庫(kù)中某些字段或字段組必須是唯一的讳窟。通過(guò)將@Index的unique 設(shè)置為true,可以強(qiáng)制執(zhí)行此唯一性屬性敞恋。

下面的代碼示例防止表有兩行包含F(xiàn)irstName和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的參數(shù) foreignKeys

因?yàn)镾QLite是一個(gè)關(guān)系型數(shù)據(jù)庫(kù)丽啡,你可以指定對(duì)象之間的關(guān)系。盡管大多數(shù)ORM庫(kù)允許實(shí)體對(duì)象相互引用硬猫,但Room明確禁止补箍。實(shí)體之間沒(méi)有對(duì)象引用改执。

不能使用直接關(guān)系,所以就要用到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;
}

外鍵是非常強(qiáng)大的辈挂,因?yàn)樗试S指定引用實(shí)體更新時(shí)發(fā)生的操作。例如裹粤,級(jí)聯(lián)刪除终蒂,你可以告訴SQLite刪除所有書籍的用戶如果用戶對(duì)應(yīng)的實(shí)例是由包括OnDelete =CASCADE在@ForeignKey注釋。ON_CONFLICT : @Insert(onConflict=REPLACE) REMOVE 或者 REPLACE

有時(shí)候可能還需要對(duì)象嵌套這時(shí)候可以用@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);
}

數(shù)據(jù)訪問(wèn)對(duì)象Data Access Objects (DAOs)是一種抽象訪問(wèn)數(shù)據(jù)庫(kù)的干凈的方式遥诉。

DAO的Insert 操作

當(dāng)創(chuàng)建DAO方法并用@Insert注釋它時(shí)拇泣,生成一個(gè)實(shí)現(xiàn)時(shí)會(huì)在一個(gè)事務(wù)中完成插入所有參數(shù)到數(shù)據(jù)庫(kù)。

@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 操作

一個(gè)簡(jiǎn)單查詢示例

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

稍微復(fù)雜的霉翔,帶參數(shù)的查詢操作

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

也可以帶多個(gè)參數(shù)

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

返回子集

上面示例都是查詢一個(gè)表中的所有字段,結(jié)果用對(duì)應(yīng)的Entity即可苞笨,但是如果我只要其中的幾個(gè)字段债朵,那么該怎么使用呢?

比如上面的User瀑凝,我只需要firstName和lastName序芦,首先定義一個(gè)子集,然后結(jié)果改成對(duì)應(yīng)子集即可猜丹。

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

支持集合參數(shù)

有個(gè)這樣一個(gè)查詢需求芝加,比如要查詢某兩個(gè)地區(qū)的所有用戶,直接用sql中的in即可射窒,但是如果這個(gè)地區(qū)是程序指定的藏杖,個(gè)數(shù)不確定,那么改怎么辦脉顿?

@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蝌麸,可以異步的獲取數(shù)據(jù),那么我們的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是另外一個(gè)異步操作庫(kù)来吩,同樣也是支持的。

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

支持Cursor

原始的Android系統(tǒng)查詢結(jié)果是通過(guò)Cursor來(lái)獲取的蔽莱,同樣也支持弟疆。

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

多表查詢

有時(shí)候數(shù)據(jù)庫(kù)存在范式相關(guān),數(shù)據(jù)拆到了多個(gè)表中盗冷,那么就需要關(guān)聯(lián)多個(gè)表進(jìn)行查詢怠苔,如果結(jié)果只是一個(gè)表的數(shù)據(jù),那么很簡(jiǎn)單仪糖,直接用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);
}

如果結(jié)果是部分字段迫肖,同上面一樣,需要單獨(dú)定義一個(gè)POJO攒驰,來(lái)接受數(shù)據(jù)蟆湖。

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

類型轉(zhuǎn)換

有時(shí)候Java定義的數(shù)據(jù)類型和數(shù)據(jù)庫(kù)中存儲(chǔ)的數(shù)據(jù)類型是不一樣的,Room提供類型轉(zhuǎn)換玻粪,這樣在操作數(shù)據(jù)庫(kù)的時(shí)候隅津,可以自動(dòng)轉(zhuǎn)換。

比如在Java中奶段,時(shí)間用Date表示饥瓷,但是在數(shù)據(jù)庫(kù)中類型確實(shí)long,這樣有利于存儲(chǔ)痹籍。

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

定義數(shù)據(jù)庫(kù)時(shí)候需要指定類型轉(zhuǎn)換呢铆,同時(shí)定義好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);
}

數(shù)據(jù)庫(kù)升級(jí)

版本迭代中蹲缠,我們不可避免的會(huì)遇到數(shù)據(jù)庫(kù)升級(jí)問(wèn)題棺克,Room也為我們提供了數(shù)據(jù)庫(kù)升級(jí)的處理接口。

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

遷移過(guò)程結(jié)束后线定,Room將驗(yàn)證架構(gòu)以確保遷移正確發(fā)生娜谊。如果Room發(fā)現(xiàn)問(wèn)題,則拋出包含不匹配信息的異常斤讥。

警告: 如果不提供必要的遷移纱皆,Room會(huì)重新構(gòu)建數(shù)據(jù)庫(kù),這意味著將丟失數(shù)據(jù)庫(kù)中的所有數(shù)據(jù)芭商。

輸出模式

可以在gradle中設(shè)置開啟輸出模式派草,便于我們調(diào)試,查看數(shù)據(jù)庫(kù)表情況铛楣,以及做數(shù)據(jù)庫(kù)遷移近迁。

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

Sample

這里是官方示例,本人自己參考官方文檔和示例Android Architecture Components samples后簸州,也寫出了一個(gè)類似的示例項(xiàng)目XiaoxiaZhihu_AAC鉴竭,還請(qǐng)多多指教。

總結(jié)

原先IO是在5月底已經(jīng)結(jié)束岸浑,本來(lái)想盡快參照官方文檔和示例搏存,把API擼一遍,然后寫個(gè)Demo和文章來(lái)介紹一下矢洲。代碼示例早已經(jīng)寫出來(lái)了璧眠,但6月分工作太忙,然后又出差到北京,最后等到了6月底了蛆橡,才把這篇文章給寫出來(lái)了。中間可能有內(nèi)容以及改變掘譬,如果有發(fā)現(xiàn)穩(wěn)重有錯(cuò)誤泰演,請(qǐng)及時(shí)指出,不理賜教葱轩。同時(shí)以后做事一定要有始有終睦焕,確定的事一定要堅(jiān)持。

相關(guān)資料

Android Architecture Components

簡(jiǎn)單聊聊Android Architecture Componets

Android Architecture Components samples

XiaoxiaZhihu_AAC

淺談Android Architecture Components

Room ORM 數(shù)據(jù)庫(kù)框架

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末靴拱,一起剝皮案震驚了整個(gè)濱河市垃喊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袜炕,老刑警劉巖本谜,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異偎窘,居然都是意外死亡乌助,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門陌知,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)他托,“玉大人,你說(shuō)我怎么就攤上這事仆葡∩筒危” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵沿盅,是天一觀的道長(zhǎng)把篓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)嗡呼,這世上最難降的妖魔是什么纸俭? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮南窗,結(jié)果婚禮上揍很,老公的妹妹穿的比我還像新娘。我一直安慰自己万伤,他們只是感情好窒悔,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敌买,像睡著了一般简珠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天聋庵,我揣著相機(jī)與錄音膘融,去河邊找鬼。 笑死祭玉,一個(gè)胖子當(dāng)著我的面吹牛氧映,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脱货,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼岛都,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了振峻?” 一聲冷哼從身側(cè)響起臼疫,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扣孟,沒(méi)想到半個(gè)月后烫堤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哈打,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年塔逃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片料仗。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡湾盗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出立轧,到底是詐尸還是另有隱情格粪,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布氛改,位于F島的核電站帐萎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏胜卤。R本人自食惡果不足惜疆导,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望葛躏。 院中可真熱鬧澈段,春花似錦、人聲如沸舰攒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)摩窃。三九已至兽叮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹦聪。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工账阻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泽本。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓宰僧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親观挎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容