[譯] Architecture Components 之 Guide to App Architecture

【目錄】

1. Architecture Components 之 Guide to App Architecture

2. Architecture Components 之 Adding Components to your Project

3. Architecture Components 之 Handling Lifecycles

4. Architecture Components 之 LiveData

5. Architecture Components 之 ViewModel

6. Architecture Components 之 Room Persistence Library

示例代碼鏈接

</br>

應(yīng)用程序架構(gòu)指南

本指南使用于具有構(gòu)建應(yīng)用程序基礎(chǔ)并且想了解構(gòu)建強(qiáng)大、優(yōu)質(zhì)的應(yīng)用程序的最佳實(shí)踐和推薦架構(gòu)的開(kāi)發(fā)人員垫桂。

注:本指南假定讀者熟悉 Android Framework。如果你是一個(gè)應(yīng)用程序開(kāi)發(fā)的新手,請(qǐng)參閱入門指南系列培訓(xùn),其中包含了本指南先決條件的相關(guān)主題拄衰。

應(yīng)用開(kāi)發(fā)者面臨的常見(jiàn)問(wèn)題

在大多數(shù)情況下,桌面應(yīng)用程序在啟動(dòng)器快捷方式中有一個(gè)單一的入口并且作為單獨(dú)的獨(dú)立進(jìn)程運(yùn)行,與桌面應(yīng)用程序不同苛让,Android 應(yīng)用具有更復(fù)雜的結(jié)構(gòu)。一個(gè)典型的 Android 應(yīng)用是由多個(gè)應(yīng)用程序組件構(gòu)成的湿诊,包括 activity狱杰,fragment,service厅须,content provider 和 broadcast receiver仿畸。

這些應(yīng)用程序組件中的大部分聲明在由 Android OS 使用的應(yīng)用程序清單中,用來(lái)決定如何將應(yīng)用融入到用戶設(shè)備的整體體驗(yàn)中朗和。盡管如前所述错沽,傳統(tǒng)的桌面應(yīng)用程序作為獨(dú)立進(jìn)程運(yùn)行,但是正確的編寫(xiě) Android 應(yīng)用程序需要更加靈活眶拉,因?yàn)橛脩魰?huì)同過(guò)設(shè)備上不同的應(yīng)用程序組織成自己的方式不斷切換流程和任務(wù)千埃。

例如,考慮下在你喜歡的社交網(wǎng)絡(luò)應(yīng)用中分享照片時(shí)會(huì)發(fā)生什么镀层。該應(yīng)用會(huì)觸發(fā)一個(gè)啟動(dòng)相機(jī)的 intent镰禾,從該 intent 中 Android OS 會(huì)啟動(dòng)一個(gè)相機(jī)應(yīng)用來(lái)處理這個(gè)請(qǐng)求皿曲。在此刻唱逢,用戶離開(kāi)社交網(wǎng)絡(luò)應(yīng)用但是用戶的體驗(yàn)是無(wú)縫的。相機(jī)應(yīng)用轉(zhuǎn)而可能會(huì)觸發(fā)其它的 intent屋休,例如啟動(dòng)文件選擇器坞古,這可能會(huì)啟動(dòng)另一個(gè)應(yīng)用。最終用戶回到社交網(wǎng)絡(luò)應(yīng)用并且分享照片劫樟。此外痪枫,在這個(gè)過(guò)程中的任何時(shí)刻用戶都有可能會(huì)被一個(gè)電話打斷织堂,并且在結(jié)束通話后再回來(lái)繼續(xù)分享照片。

在 Android 中奶陈,這種應(yīng)用切換行為很常見(jiàn)易阳,所以你的應(yīng)用程序必須正確處理這些流程。記住吃粒,移動(dòng)設(shè)備的資源是有限的潦俺,所以在任何時(shí)候,操作系統(tǒng)都可能會(huì)殺死一些應(yīng)用為新的應(yīng)用騰出空間徐勃。

其中的重點(diǎn)是應(yīng)用程序組件可能會(huì)被單獨(dú)和無(wú)序的啟動(dòng)事示,并且可能會(huì)被用戶或系統(tǒng)在任何時(shí)候銷毀。因?yàn)閼?yīng)用程序組件是短暫的僻肖,并且其聲明周期(什么時(shí)候被創(chuàng)建和銷毀)不受你控制肖爵,所以不應(yīng)該在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用數(shù)據(jù)或狀態(tài),同時(shí)應(yīng)用程序組件不應(yīng)該相互依賴臀脏。

常見(jiàn)的架構(gòu)原則

如果不能在應(yīng)用程序組件中存儲(chǔ)應(yīng)用數(shù)據(jù)和狀態(tài)劝堪,那么應(yīng)該如何構(gòu)建應(yīng)用?

最重要的是在應(yīng)用中要專注于關(guān)注點(diǎn)分離揉稚。一個(gè)常見(jiàn)的錯(cuò)誤是在 ActivityFragment 中編寫(xiě)所有的代碼幅聘。任何不是處理 UI 或 操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中。保持它們盡可能的精簡(jiǎn)可以避免許多與生命周期有關(guān)的問(wèn)題窃植。不要忘記你不擁有這些類帝蒿,它們只是體現(xiàn)了 OS 和 應(yīng)用之間協(xié)議的粘合類。Android OS 可能會(huì)因?yàn)橛脩艚换セ蚱渌蛩兀ㄈ绲蛢?nèi)存)的原因在任何時(shí)候銷毀它們巷怜。最好盡量減少對(duì)它們的依賴以提供一個(gè)穩(wěn)固的用戶體驗(yàn)葛超。

第二個(gè)重要的原則是應(yīng)該用 Model 驅(qū)動(dòng) UI,最好是持久化的 Model延塑。持久化是最佳的原因有兩個(gè):一是如果 OS 銷毀應(yīng)用釋放資源绣张,用戶不用擔(dān)心丟失數(shù)據(jù);二是即使網(wǎng)絡(luò)連接不可靠或者是斷開(kāi)的关带,應(yīng)用仍將繼續(xù)運(yùn)行侥涵。Model 是負(fù)責(zé)處理應(yīng)用數(shù)據(jù)的組件。Modle 獨(dú)立于應(yīng)用中的 View 和應(yīng)用程序組件宋雏,因此 Model 和這些組件的生命周期問(wèn)題隔離開(kāi)了芜飘。保持 UI 代碼精簡(jiǎn)并且摒除應(yīng)用的邏輯使其更易于管理∧プ埽基于 Model 類構(gòu)建的應(yīng)用程序其管理數(shù)據(jù)的職責(zé)明確嗦明,使應(yīng)用程序可測(cè)試并且穩(wěn)定。

推薦的應(yīng)用程序架構(gòu)

在本節(jié)中蚪燕,我們將通過(guò)一個(gè)用例來(lái)演示如何使用 Architecture Components 構(gòu)建應(yīng)用程序娶牌。

注:不可能有一種應(yīng)用程序的編寫(xiě)方式對(duì)于每種情況都是最好的奔浅。話雖如此,這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的良好起點(diǎn)诗良。如果你已經(jīng)有一種很好的應(yīng)用程序編寫(xiě)方式則不需要改變汹桦。

假設(shè)我們正在構(gòu)建一個(gè)顯示用戶個(gè)人信息的 UI。用戶的個(gè)人信息將使用 REST API 從我們自己的私有后端獲取鉴裹。

構(gòu)建用戶界面

UI 包含一個(gè) fragment 文件 UserProfileFragment.java 和其布局文件 user_profile_layout.xml营勤。

為了驅(qū)動(dòng) UI,數(shù)據(jù)模型需要持有兩個(gè)數(shù)據(jù)元素壹罚。

  • 用戶 ID :用戶的標(biāo)識(shí)符葛作。最好使用 fragment 的參數(shù)將用戶 ID 傳到 fragment 中。如果 Android OS 銷毀進(jìn)程猖凛,該 ID 將會(huì)被保存赂蠢,以便下次應(yīng)用重啟時(shí)該 ID 可用。

  • 用戶對(duì)象:保存用戶數(shù)據(jù)的普通 Java 對(duì)象(POJO)辨泳。

我們將會(huì)基于 ViewModel 來(lái)創(chuàng)建一個(gè) UserProfileViewModel 來(lái)保存這些信息虱岂。

<p><strong><a >ViewModel</a></strong> 為指定的 UI 組件(如:fragment 或 activity)提供數(shù)據(jù),并且負(fù)責(zé)與數(shù)據(jù)處理的業(yè)務(wù)部分的交互菠红,例如:調(diào)用其它組件獲取數(shù)據(jù)或轉(zhuǎn)發(fā)用戶的操作第岖。ViewModel 對(duì)于 View 并不了解并且不受配置改變(如:由于旋轉(zhuǎn)導(dǎo)致 activity 的重新創(chuàng)建)的影響</p>

現(xiàn)在有 3 個(gè)文件

  • user_profile.xml:屏幕上的 UI 定義。

  • UserProfileViewModel.java:為 UI 準(zhǔn)備數(shù)據(jù)的類试溯。

  • UserProfileFragment.java:用于在 ViewModel 中顯示數(shù)據(jù)并對(duì)用戶交互做出反應(yīng)的 UI 控制器蔑滓。

下面是我們的初始實(shí)現(xiàn)(簡(jiǎn)單起見(jiàn)省略布局文件):

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends LifecycleFragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

注:上面的例子繼承了 LifecycleFragment 而不是 Fragment 類。在 Architecture Components 中的生命周期 API 穩(wěn)定后遇绞, Android 支持包中的 Fragment 類將會(huì)實(shí)現(xiàn) LifecycleOwner 接口键袱。

現(xiàn)在我們有了 3 個(gè)代碼模塊,怎樣連接它們摹闽?最后蹄咖,當(dāng) ViewModel 的用戶字段被設(shè)置時(shí),需要一種方式來(lái)通知 UI付鹿。這正是 LiveData 的用武之地澜汤。

<p><strong><a >LiveData</a></strong> 是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察 <strong><a >LiveData</a></strong> 進(jìn)行改變舵匾,而不會(huì)在組件之間創(chuàng)建顯示的俊抵,固定的依賴。另外纽匙,LiveData 還遵守應(yīng)用程序組件(如:activity务蝠,fragment拍谐,service)的生命周期狀態(tài)烛缔,并且防止對(duì)象泄漏使應(yīng)用不會(huì)消耗更多的內(nèi)存馏段。</p>

注:如果你已經(jīng)再使用像 RxJavaAgera 的庫(kù),你可以繼續(xù)使用它們而不用換成 LiveData践瓷。但是當(dāng)使用它們或其它的方式時(shí)院喜,請(qǐng)確保正確處理生命周期,如:當(dāng)相關(guān) LifecycleOwner 停止時(shí)暫停數(shù)據(jù)流或在 LifecycleOwner 被銷毀時(shí)銷毀數(shù)據(jù)流晕翠∨缫ǎ可以添加 android.arch.lifecycle:reactivestreams 工具,和其它的響應(yīng)流庫(kù)(如:RxJava2)一起使用 LiveData淋肾。

將 UserProfileViewModel 中的 User 字段替換為 LiveData <User>硫麻,以便在更新數(shù)據(jù)時(shí)可以通知 fragment。 LiveData 的好處在于它是生命周期感知的樊卓,并且可以在不再被需要的時(shí)候自動(dòng)清除引用拿愧。

public class UserProfileViewModel extends ViewModel {
    ...
    // private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

修改 UserprofileFragment 來(lái)觀察數(shù)據(jù)并更新 UI。

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // 更新 UI
    });
}

每次更新用戶數(shù)據(jù)時(shí)碌尔,將會(huì)調(diào)用 onChange 回調(diào)并且更新 UI浇辜。

如果你熟悉其它庫(kù)的可觀察回調(diào)的使用,可能已經(jīng)意識(shí)到我們沒(méi)有重寫(xiě) fragment 的 onStop() 方法來(lái)停止觀察數(shù)據(jù)唾戚。這對(duì)于 LiveData 來(lái)說(shuō)是不必要的柳洋,因?yàn)?LiveData 是證明周期感知的,這意味著除非 fragment 處于活動(dòng)狀態(tài)(收到了 onStart() 但還沒(méi)有收到 onStop())叹坦,否則它不會(huì)調(diào)用回調(diào)熊镣。當(dāng) fragment 收到 onDestroy() 時(shí) LiveData 會(huì)自動(dòng)移除觀察者。

我們沒(méi)有做任何事情來(lái)特別是處理配置的變化(如:用戶旋轉(zhuǎn)屏幕)募书。當(dāng)配置發(fā)生變化時(shí) ViewModel 將會(huì)自動(dòng)恢復(fù)轧钓,所以,只要新的 fragment 啟動(dòng)锐膜,它將會(huì)收到屬于 ViewModel 的相同實(shí)例毕箍,并且使用最新的數(shù)據(jù)立即調(diào)用回調(diào)。這就是為什么 ViewModel 不應(yīng)該直接引用 View道盏,ViewModel 可能存活的比 View 的生命周期長(zhǎng)而柑。請(qǐng)參閱 ViewModel 的生命周期

獲取數(shù)據(jù)

我們已經(jīng)將 ViewModel 鏈接到了 fragment荷逞,但是 ViewModel 怎樣獲取數(shù)據(jù)呢媒咳?這個(gè)例子中,假設(shè)我們的后端提供了一個(gè) REST API种远。我們將會(huì)使用 Retrofit 庫(kù)來(lái)訪問(wèn)后端涩澡,你也可以自由的使用其它庫(kù)來(lái)達(dá)到同樣的目的。

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

一個(gè)簡(jiǎn)單的 ViewModel 實(shí)現(xiàn)可以直接調(diào)用 Webservice 獲取數(shù)據(jù)并將其分配給用戶對(duì)象妙同。即使這樣可以使用射富,但是應(yīng)用程序?qū)?huì)隨著增長(zhǎng)而難以維護(hù)。將太多的職責(zé)交給 ViewModel 這違反了我們前面提到的關(guān)注點(diǎn)分離的原則粥帚。此外胰耗,ViewModel 的作用域依賴于 ActivityFragment 的生命周期,因此在 ActivityFragment 的生命周期結(jié)束時(shí)丟失所有的數(shù)據(jù)是一種不好的用戶體驗(yàn)芒涡。故而柴灯,我們的 ViewModel 將把這項(xiàng)工作委托給一個(gè)新的 Repository 模塊。

<p><strong>Repository</strong> 模塊負(fù)責(zé)處理數(shù)據(jù)操作费尽。它們?yōu)閼?yīng)用程序的其它部分提供了一個(gè)干凈的 API赠群。它們知道在數(shù)據(jù)更新時(shí)從哪里獲取數(shù)據(jù)和調(diào)用哪些 API『涤祝可以將其視為不同數(shù)據(jù)源(持久化模型乎串,Web 服務(wù)苟弛,緩存等)之間的中間層角寸。</p>

下面的 UserRepository 類使用 WebService 來(lái)獲取用戶數(shù)據(jù)。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // 這是最佳的實(shí)現(xiàn)忿项,下面會(huì)有解釋
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // 為了簡(jiǎn)單起見(jiàn)省略錯(cuò)誤的情況
                data.setValue(response.body());
            }
        });
        return data;
    }
}

雖然 Repository 模塊看起來(lái)是不必要的闷旧,但是它起著一個(gè)重要的作用长豁;它抽象了應(yīng)用程序其它部分的數(shù)據(jù)源。現(xiàn)在 ViewModel 不知道數(shù)據(jù)是由 Webservice 獲取的忙灼,這意味著可以根據(jù)需求將其切換為其它實(shí)現(xiàn)匠襟。

注:為了簡(jiǎn)單起見(jiàn),我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況该园。有關(guān)于暴露錯(cuò)誤和加載狀態(tài)的可選實(shí)現(xiàn)方式酸舍,請(qǐng)參閱附錄:暴露網(wǎng)絡(luò)狀態(tài)

管理組件之間的依賴

上面的 UserRepository 類需要一個(gè) Webservice 的實(shí)例來(lái)完成其工作里初】忻悖可以簡(jiǎn)單的創(chuàng)建 Webservice,但是這需要知道 Webservice 的依賴來(lái)構(gòu)造它双妨。這將會(huì)顯著的使代碼復(fù)雜和重復(fù)(例如:需要 Webservice 實(shí)例的每個(gè)類都需要知道如何使用它的依賴來(lái)構(gòu)造它)淮阐。另外,UserRepostory 可能不是唯一需要 Webservice 的類刁品。如果每個(gè)類都創(chuàng)建一個(gè)新的 Webservice泣特,這將會(huì)造成非常大的資源負(fù)擔(dān)。

有兩種模式可以解決這個(gè)問(wèn)題:

  • 依賴注入:依賴注入允許類定義其依賴而不構(gòu)造它們挑随。在運(yùn)行時(shí)状您,另一個(gè)類負(fù)責(zé)提供這些依賴。推薦使用 Google 的 Dagger 2 庫(kù)在 Android 應(yīng)用中實(shí)現(xiàn)依賴注入。Dagger 2 通過(guò)遍歷依賴關(guān)系樹(shù)自動(dòng)構(gòu)建對(duì)象并為依賴提供編譯時(shí)保障膏孟。

  • 服務(wù)定位:服務(wù)定位提供了一個(gè)注冊(cè)表眯分,類可以從中獲取它們的依賴關(guān)系,而不是構(gòu)造它們骆莹。與依賴注入(DI)相比颗搂,服務(wù)定位實(shí)現(xiàn)起來(lái)相對(duì)容易担猛,所以如果不熟悉 DI幕垦,請(qǐng)使用服務(wù)定位代替。

這些模式允許你擴(kuò)展代碼傅联,因?yàn)樗鼈兲峁┣逦哪J絹?lái)管理依賴關(guān)系先改,而不會(huì)重復(fù)代碼或增加復(fù)雜性。兩者都允許替換實(shí)現(xiàn)進(jìn)行測(cè)試蒸走;這是使用它們的主要好處之一仇奶。

在這個(gè)例子中,我們將使用 Dagger 2 來(lái)管理依賴比驻。

連接 ViewModel 和 Repository

修改 UserProfileViewModel 以使用 Repository该溯。

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

    @Inject // UserRepository 參數(shù)由 Dagger 2 提供
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel 是由 Fragment 創(chuàng)建的
            // 所以我們知道 userId 不會(huì)改變
            return;
        }
        user = userRepo.getUser(userId);
    }

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

緩存數(shù)據(jù)

上述 Repository 的實(shí)現(xiàn)對(duì)于抽象調(diào)用 Web 服務(wù)是很好的,但是因?yàn)樗鼉H依賴于一個(gè)數(shù)據(jù)源所以不是很實(shí)用别惦。

上述 UserRepository 的實(shí)現(xiàn)的問(wèn)題是在獲取到數(shù)據(jù)之后沒(méi)有把數(shù)據(jù)保存下來(lái)狈茉。如果用戶離開(kāi) UserProfileFragment 然后返回回來(lái),應(yīng)用將會(huì)重新獲取數(shù)據(jù)掸掸。這樣是很不好的氯庆,原因有兩個(gè):一是浪費(fèi)了寶貴的網(wǎng)絡(luò)帶寬;二是迫使用戶等待新的查詢完成扰付。為了解決這個(gè)問(wèn)題堤撵,我們將在 UserRepository 中添加一個(gè)新的數(shù)據(jù)源,用以在內(nèi)存中緩存 User 對(duì)象羽莺。

@Singleton  // 告訴 Dagger 這個(gè)類只應(yīng)該構(gòu)造一次
public class UserRepository {
    private Webservice webservice;
    // 簡(jiǎn)單的內(nèi)存緩存实昨,為了簡(jiǎn)單忽略相關(guān)細(xì)節(jié)
    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);
        // 這還不是最好的但比以前的好。
        // 一個(gè)完整的實(shí)現(xiàn)必須處理錯(cuò)誤的情況盐固。
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

持久化數(shù)據(jù)

在當(dāng)前的實(shí)現(xiàn)中屠橄,如果用戶旋轉(zhuǎn)屏幕或離開(kāi)并返回應(yīng)用,已存在的 UI 將會(huì)立即可見(jiàn)闰挡,因?yàn)?Repository 從內(nèi)存緩存中取回?cái)?shù)據(jù)锐墙。但是,如果用戶離開(kāi)應(yīng)用并在幾個(gè)小時(shí)后(Android OS 已經(jīng)殺死進(jìn)程后)返回又會(huì)發(fā)生什么长酗?

如果是目前的實(shí)現(xiàn)溪北,將會(huì)需要再次從網(wǎng)絡(luò)獲取數(shù)據(jù)。這不僅是一個(gè)不好的用戶體驗(yàn),并且也是浪費(fèi)之拨,因?yàn)檫@將會(huì)使用移動(dòng)數(shù)據(jù)重新獲取同樣的數(shù)據(jù)茉继。可以通過(guò)緩存 Web 請(qǐng)求來(lái)簡(jiǎn)單的解決這個(gè)問(wèn)題蚀乔,但是這會(huì)導(dǎo)致新的問(wèn)題烁竭。如果相同的用戶數(shù)據(jù)來(lái)自另一種請(qǐng)求并顯示(例如:獲取朋友列表)將會(huì)怎樣?這時(shí)應(yīng)用會(huì)顯示不一致的數(shù)據(jù)吉挣,這是最令人困惑的用戶體驗(yàn)派撕。例如:相同用戶的數(shù)據(jù)可能會(huì)顯示的不同,因?yàn)榕笥蚜斜淼恼?qǐng)求和用戶個(gè)人信息的請(qǐng)求可能會(huì)在不同的時(shí)間執(zhí)行睬魂。應(yīng)用程序需要合并它們以避免顯示不一致的數(shù)據(jù)终吼。

解決這個(gè)問(wèn)題的正確方法是使用持久化模型。這就是持久化庫(kù) Room 的用武之地了氯哮。

<p><strong><a >Room</a></strong> 是一個(gè)以最少的樣板代碼提供本地?cái)?shù)據(jù)持久化的對(duì)象映射庫(kù)际跪。在編譯時(shí),它會(huì)根據(jù)模式驗(yàn)證每個(gè)查詢喉钢,所以損壞的 SQL 查詢只會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤姆打,而不是運(yùn)行時(shí)崩潰。Room 抽象出一些使用原始 SQL 表查詢的底層實(shí)現(xiàn)細(xì)節(jié)肠虽。它還允許觀察數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢)的變化幔戏,并通過(guò) <em>LiveData</em> 對(duì)象暴露這些變化。另外舔痕,它明確定義了線程約束以解決常見(jiàn)問(wèn)題(如在主線程訪問(wèn)存儲(chǔ))评抚。</p>

注:如果你熟悉其它的持久化解決方案,如:SQLite ORM 或像 Realm 等其他的數(shù)據(jù)庫(kù)伯复,你不需要用更換為 Room慨代,除非 Room 的功能對(duì)你的用例更加適用。

使用 Room啸如,需要定義我們的局部模式侍匙。首先,用 @Entity 注釋 User 類叮雳,將其標(biāo)記為數(shù)據(jù)庫(kù)中的一個(gè)表想暗。

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // 字段的 get 和 set 方法
}

然后,通過(guò)繼承 RoomDatabase 為應(yīng)用創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)帘不。

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

請(qǐng)注意说莫,MyDatabase 是抽象類,Room 會(huì)自動(dòng)提供其實(shí)現(xiàn)類寞焙。詳細(xì)信息請(qǐng)參閱 Room 的文檔储狭。

現(xiàn)在需要一種方法來(lái)將用戶數(shù)據(jù)插入數(shù)據(jù)庫(kù)互婿。為此,我們將創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象( DAO )辽狈。

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

然后慈参,在數(shù)據(jù)庫(kù)類中引用 DAO。

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

請(qǐng)注意刮萌,load 方法的返回值是 LiveData<User>驮配。Room 知道數(shù)據(jù)庫(kù)何時(shí)被修改,當(dāng)數(shù)據(jù)發(fā)生變化時(shí)着茸,它會(huì)通知所有處于活動(dòng)狀態(tài)的觀察者壮锻。使用 LiveData 是高效的是因?yàn)樗挥性谥辽儆幸粋€(gè)處于活動(dòng)狀態(tài)的觀察者時(shí)才會(huì)更新數(shù)據(jù)。

注:從 alpha 1 版本開(kāi)始元扔,Room 基于表修改的檢查無(wú)效躯保,這意味著它可能會(huì)發(fā)送錯(cuò)誤的通知旋膳。

現(xiàn)在修改 UserRepository 來(lái)整合 Room 的數(shù)據(jù)源澎语。

@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);
        // 直接從數(shù)據(jù)庫(kù)返回一個(gè) LiveData。
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // 在后臺(tái)線程中運(yùn)行
            // 檢查最近是否獲取過(guò) user
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // 刷新數(shù)據(jù)
                Response response = webservice.getUser(userId).execute();
                // TODO 檢查錯(cuò)誤等验懊。
                // 更新數(shù)據(jù)庫(kù)擅羞。LiveData 將會(huì)自動(dòng)刷新
                // 所以除了更新數(shù)據(jù)庫(kù)外不需要任何操作。
                userDao.save(response.body());
            }
        });
    }
}

請(qǐng)注意义图,即使我們更改了 UserRepository 中的數(shù)據(jù)來(lái)源减俏,我們也不需要更改 UserProfileViewModel 或 UserProfileFragment。這是抽象帶來(lái)的靈活性碱工。這樣也非常易于測(cè)試娃承,因?yàn)樵跍y(cè)試 UserProfileViewModel 可以提供一個(gè)假的 UserRepository。

現(xiàn)在我們的代碼是完整的怕篷,如果用戶日后再回到相同的 UI历筝,他們會(huì)立即看到用戶信息,因?yàn)槲覀円呀?jīng)將其持久化了廊谓。同時(shí)梳猪,如果數(shù)據(jù)過(guò)期,Repository 將會(huì)在后臺(tái)更新數(shù)據(jù)蒸痹。當(dāng)然春弥,根據(jù)你的用例,如果持久化的數(shù)據(jù)太舊你可能不希望顯示它們叠荠。

在一些用例中匿沛,如下拉刷新,在進(jìn)行網(wǎng)絡(luò)操作時(shí)顯示用戶數(shù)據(jù)對(duì)于 UI 來(lái)說(shuō)非常重要榛鼎。將 UI 操作從實(shí)際數(shù)據(jù)中分離是一個(gè)很好的做法逃呼,因?yàn)槠淇赡苡捎诟鞣N原因而被更新(例如:如果獲取一個(gè)朋友列表公给,可能會(huì)再次獲取到相同的 user 并觸發(fā) LiveData<User> 更新)。從 UI 的角度來(lái)看蜘渣,動(dòng)態(tài)請(qǐng)求實(shí)際上只是另一個(gè)數(shù)據(jù)點(diǎn)淌铐,類似其它任何數(shù)據(jù)片段(如:User 對(duì)象)

這種用例有兩種常見(jiàn)的解決方案:

  • 更改 getUser 以返回包含網(wǎng)絡(luò)操作狀態(tài)的 LiveData。在附錄:暴露網(wǎng)絡(luò)狀態(tài)部分提供了一個(gè)實(shí)現(xiàn)例子蔫缸。

  • 在 Repository 類中提供一個(gè)可以返回 User 刷新?tīng)顟B(tài)的公共方法腿准。如果只是為了響應(yīng)明確的用戶操作而在 UI 中顯示網(wǎng)絡(luò)狀態(tài)(如:下拉刷新),則這種方式是更好的選擇拾碌。

單一數(shù)據(jù)源

不同的 REST API 接口返回相同的數(shù)據(jù)是很常見(jiàn)的吐葱。例如,如果后端有另一個(gè)返回朋友列表的接口校翔,相同的用戶對(duì)象(也許是不同的粒度)可能來(lái)自兩個(gè)不同的 API 接口弟跑。如果 UserRepository 把從 Webservice 請(qǐng)求獲取到的響應(yīng)原樣返回,碼么 UI 可能會(huì)顯示不一致的數(shù)據(jù)防症,因?yàn)樵谶@些請(qǐng)求之間數(shù)據(jù)可能在服務(wù)端發(fā)生了改變孟辑。這就是為什么在 UserRepository 的實(shí)現(xiàn)中 Web 服務(wù)的回調(diào)只是將數(shù)據(jù)保存到了數(shù)據(jù)庫(kù)。然后蔫敲,對(duì)數(shù)據(jù)庫(kù)的更改將會(huì)觸發(fā)處于活動(dòng)狀態(tài)的 LiveData 對(duì)象上的回調(diào)饲嗽。

在這個(gè)模型中,數(shù)據(jù)庫(kù)服務(wù)作為單一數(shù)據(jù)源奈嘿,應(yīng)用程序的其它部分通過(guò) Repository 來(lái)訪問(wèn)它貌虾。無(wú)論是否是否磁盤緩存,建議 Repository 指定一個(gè)數(shù)據(jù)源作為應(yīng)用程序其它部分的單一數(shù)據(jù)源裙犹。

測(cè)試

我們已經(jīng)提到分離的好處之一是可測(cè)試性尽狠。讓我們看看如何測(cè)試每個(gè)代碼模塊。

  • 用戶界面或用戶交互:這將是唯一一次需要 Android UI Instrumentation test叶圃。測(cè)試 UI 代碼的最佳方式是創(chuàng)建一個(gè) Espresso 測(cè)試袄膏。可以創(chuàng)建一個(gè) fragment 并為其提供一個(gè)模擬的 ViewModel盗似。因?yàn)?fragment 只和 ViewModel 交互哩陕,隨意模擬 ViewModel 足以完全測(cè)是 UI。

  • ViewModel:可以使用 JUnit test 來(lái)測(cè)試 ViewModel赫舒。只需要模擬 UserRepository 來(lái)測(cè)試它悍及。

  • UserRepository:也可以使用 JUnit test 來(lái)測(cè)試 UserRepository。需要模擬 Webservice 和 DAO接癌⌒母希可以測(cè)試 UserRepository 是否進(jìn)行了正確的 Web 服務(wù)調(diào)用,將結(jié)構(gòu)保存到數(shù)據(jù)庫(kù)缺猛,如果數(shù)據(jù)被緩存且是最新的缨叫,則不會(huì)發(fā)起任何不必要的請(qǐng)求椭符。因?yàn)?WebService 和 UserDao 都是接口,所以可以模擬它們或者為更復(fù)雜的測(cè)試用例創(chuàng)建假的實(shí)現(xiàn)耻姥。

  • UserDao:推薦使用 Instrumentation 測(cè)試的方式測(cè)試 DAO 類销钝。因?yàn)?Instrumentation 測(cè)試不需要任何 UI,它們會(huì)運(yùn)行的很快琐簇。對(duì)于每個(gè)測(cè)試蒸健,可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù)以確保測(cè)試沒(méi)有任何副作用(例如:改變磁盤上的數(shù)據(jù)庫(kù)文件)。

    Room 還允許指定數(shù)據(jù)庫(kù)實(shí)現(xiàn)婉商,所以可以通過(guò)向其提供 SupportSQLiteOpenHelper 的 JUnit 實(shí)現(xiàn)來(lái)測(cè)試它似忧。通常不推薦這種方式,因?yàn)樵O(shè)備上運(yùn)行的 SQLite 版本可能和主機(jī)上的 SQLite 版本不同丈秩。

  • Webservice:重點(diǎn)是使測(cè)試相對(duì)于外部獨(dú)立盯捌,所以 Webservice 的測(cè)試要避免通過(guò)網(wǎng)絡(luò)調(diào)用后端。有許多庫(kù)可以幫助完成該測(cè)試蘑秽。例如:MockWebServer 是一個(gè)很好的庫(kù)饺著,可以幫助為測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)。

  • 測(cè)試工件架構(gòu)組件提供了一個(gè) maven 工件來(lái)控制其后臺(tái)線程筷狼。在 android.arch.core:core-testing 工件中瓶籽,有兩個(gè) JUnit 規(guī)則:

    • InstantTaskExecutorRule:該規(guī)則可用于強(qiáng)制架構(gòu)組件立即執(zhí)行調(diào)用線程上的任何后臺(tái)操作匠童。

    • CountingTaskExecutorRule:該規(guī)則可用于 Instrumentation 測(cè)試埂材,以等待架構(gòu)組件的后臺(tái)操作,或?qū)⑵溥B接到 Espresso 作為閑置資源汤求。

最終的架構(gòu)

下圖顯示了推薦架構(gòu)中的所有模塊以及它們?nèi)绾位ハ嘟换ィ?/p>

指導(dǎo)原則

編程是一個(gè)創(chuàng)作領(lǐng)域俏险,構(gòu)建 Android 應(yīng)用也不例外。有許多方法來(lái)解決問(wèn)題扬绪,無(wú)論是在多個(gè) activity 或 fragment 之間傳遞數(shù)據(jù)竖独,是獲取遠(yuǎn)程數(shù)據(jù)并為了離線模式將其持久化到本地,還是特殊應(yīng)用遭遇的其它常見(jiàn)情況挤牛。

雖然一下建議不是強(qiáng)制性的莹痢,但是以我們的經(jīng)驗(yàn),從長(zhǎng)遠(yuǎn)來(lái)看墓赴,遵循這些建議將會(huì)使代碼庫(kù)更健壯竞膳,易測(cè)試和易維護(hù)。

  • 在 manifest 中定義的入口點(diǎn)诫硕,如:acitivy坦辟,fragment,broadcast receiver 等章办,不是數(shù)據(jù)源锉走。相反滨彻,它們應(yīng)該只是協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集。由于每個(gè)應(yīng)用程序組件的存活時(shí)間很短挪蹭,這取決于用戶與其設(shè)備的交互以及運(yùn)行時(shí)的總體狀況亭饵,所以任何入口點(diǎn)都不應(yīng)該成為數(shù)據(jù)源。

  • 嚴(yán)格的在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確的責(zé)任界限梁厉。例如:不在代碼庫(kù)中的多個(gè)類或包中擴(kuò)散從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼冬骚。同樣,不要將無(wú)關(guān)的責(zé)任(如:數(shù)據(jù)緩存和數(shù)據(jù)綁定)放到同一個(gè)類中懂算。

  • 每個(gè)模塊盡可能少的暴露出來(lái)只冻。不要視圖創(chuàng)建暴露模塊內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的“只一個(gè)”的快捷方式。你可能會(huì)在短期內(nèi)節(jié)省一些時(shí)間计技,但是隨著代碼庫(kù)的發(fā)展喜德,你將會(huì)多次償還更多的基數(shù)債務(wù)。

  • 當(dāng)定義模塊間的交互時(shí)垮媒,請(qǐng)考慮如何讓每個(gè)模塊可以獨(dú)立的測(cè)試舍悯。例如,擁有一個(gè)用于從網(wǎng)絡(luò)獲取數(shù)據(jù)且定義良好的 API 的模塊睡雇,將會(huì)使其更易于測(cè)試在本地?cái)?shù)據(jù)庫(kù)中持久化數(shù)據(jù)萌衬。相反,如果將兩個(gè)模塊的邏輯放在一個(gè)地方它抱,或者將網(wǎng)絡(luò)代碼擴(kuò)散到整個(gè)代碼庫(kù)秕豫,測(cè)試將會(huì)變的非常困難(并非不可能)。

  • 應(yīng)用程序的核心是使其脫穎而出观蓄。不要花費(fèi)時(shí)間重復(fù)造輪子或一次又一次的編寫(xiě)相同的樣板代碼混移。相反,將精力集中在使應(yīng)用程序獨(dú)一無(wú)二上侮穿,讓 Android Architecture Components 和其它的優(yōu)秀的庫(kù)來(lái)處理重復(fù)的樣板代碼歌径。

  • 持久化盡可能多的相關(guān)最新數(shù)據(jù),以便應(yīng)用程序在設(shè)備處于離線模式時(shí)還可以使用亲茅。即使你可以享用穩(wěn)定高速的網(wǎng)絡(luò)連接回铛,但是你的用戶可能無(wú)法享用。

  • Repository 應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一數(shù)據(jù)源克锣。每當(dāng)應(yīng)用程序需要訪問(wèn)數(shù)據(jù)時(shí)茵肃,數(shù)據(jù)應(yīng)該始終來(lái)源于單一數(shù)據(jù)源。有關(guān)更多信息娶耍,請(qǐng)參閱單一數(shù)據(jù)源

附錄:暴露網(wǎng)絡(luò)狀態(tài)

在上面推薦的應(yīng)用程序架構(gòu)部分免姿,為了保持示例簡(jiǎn)單我們故意忽略網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài)。在本節(jié)中榕酒,我們演示一種通過(guò) Resource 類暴露網(wǎng)絡(luò)狀態(tài)來(lái)封裝數(shù)據(jù)和其狀態(tài)胚膊。

以下是一個(gè)實(shí)現(xiàn)的例子:

// 描述數(shù)據(jù)和其狀態(tài)的類
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

因?yàn)閺拇疟P中獲取并顯示數(shù)據(jù)同時(shí)再?gòu)木W(wǎng)絡(luò)獲取數(shù)據(jù)是一種常見(jiàn)的用例故俐。我們將創(chuàng)建一個(gè)可以在多個(gè)地方使用的幫助類 NetworkBoundResource。下面是 NetworkBoundResource 的決策樹(shù)紊婉。

它通過(guò)觀察資源的數(shù)據(jù)庫(kù)药版。當(dāng)首次從數(shù)據(jù)庫(kù)加載條目時(shí),NetworkBoundResource 檢查返回結(jié)果是否足夠好可以被發(fā)送和(或)應(yīng)該從網(wǎng)絡(luò)獲取數(shù)據(jù)喻犁。請(qǐng)注意槽片,他們可能同時(shí)發(fā)生,因?yàn)槟憧赡軙?huì)希望在顯示緩存數(shù)據(jù)的同時(shí)從網(wǎng)絡(luò)更新數(shù)據(jù)肢础。

如果網(wǎng)絡(luò)調(diào)用成功还栓,則將返回?cái)?shù)據(jù)保存到數(shù)據(jù)庫(kù)中并重新初始化數(shù)據(jù)流。如果網(wǎng)絡(luò)請(qǐng)求失敗传轰,直接發(fā)送一個(gè)錯(cuò)誤剩盒。

注:將新的數(shù)據(jù)保存到磁盤后,要從數(shù)據(jù)庫(kù)重新初始化數(shù)據(jù)流慨蛙,但是通常不需要這樣做辽聊,因?yàn)閿?shù)據(jù)庫(kù)將會(huì)發(fā)送變更。另一方面期贫,依賴數(shù)據(jù)庫(kù)發(fā)送變更會(huì)有一些不好的副作用跟匆,因?yàn)樵跀?shù)據(jù)沒(méi)有變化時(shí)如果數(shù)據(jù)庫(kù)會(huì)避免發(fā)送更改將會(huì)使其中斷。我們也不希望發(fā)送從網(wǎng)絡(luò)返回的結(jié)果通砍,因?yàn)檫@違背的單一數(shù)據(jù)源原則(即使在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變保存值)玛臂。我們也不希望在沒(méi)有新數(shù)據(jù)的時(shí)候發(fā)送 SUCCESS,因?yàn)檫@會(huì)給客戶端發(fā)送錯(cuò)誤信息埠帕。

以下是 NetworkBoundResource 類為其子類提供的公共 API:

// ResultType: Resource 數(shù)據(jù)的類型
// RequestType: API 響應(yīng)的類型
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // 調(diào)用該方法將 API 響應(yīng)的結(jié)果保存到數(shù)據(jù)庫(kù)中垢揩。
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // 調(diào)用該方法判斷數(shù)據(jù)庫(kù)中的數(shù)據(jù)是否應(yīng)該從網(wǎng)絡(luò)獲取并更新。
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // 調(diào)用該方法從數(shù)據(jù)庫(kù)中獲取緩存數(shù)據(jù)敛瓷。
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // 調(diào)用該方法創(chuàng)建 API 請(qǐng)求。
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // 獲取失敗時(shí)調(diào)用斑匪。
    // 子類可能需要充值組件(如:速率限制器)呐籽。
    @MainThread
    protected void onFetchFailed() {
    }

    // 返回一個(gè)代表 Resource 的 LiveData。
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

請(qǐng)注意蚀瘸,上述類定義了兩個(gè)類型參數(shù)(ResultType狡蝶,RequestType),因?yàn)閺?API 返回的數(shù)據(jù)類型可能和本地使用的數(shù)據(jù)類型不同贮勃。

還要注意贪惹,上述代碼使用 ApiResponse 作為網(wǎng)絡(luò)請(qǐng)求,ApiResponse 是對(duì)于 Retrofit2.Call 類的簡(jiǎn)單封裝寂嘉,用以將其響應(yīng)轉(zhuǎn)換為 LiveData奏瞬。

以下是 NetworkBoundResource 類的其余實(shí)現(xiàn)部分枫绅。

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // 重新附加 dbSource 作為新的來(lái)源,
        // 它將會(huì)迅速發(fā)送最新的值。
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // 指定請(qǐng)求一個(gè)最新的實(shí)時(shí)數(shù)據(jù)硼端。
                // 否則并淋,會(huì)得到最新的緩存數(shù)據(jù),并且可能不會(huì)由從網(wǎng)絡(luò)獲取的最新數(shù)據(jù)更新珍昨。
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }
}

現(xiàn)在县耽,可以使用 NetworkBoundResource 在 Repository 中編寫(xiě)磁盤和網(wǎng)絡(luò)綁定 User 的實(shí)現(xiàn)。

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末镣典,一起剝皮案震驚了整個(gè)濱河市兔毙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兄春,老刑警劉巖瞒御,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異神郊,居然都是意外死亡肴裙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門涌乳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜻懦,“玉大人,你說(shuō)我怎么就攤上這事夕晓⊥鹉耍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵蒸辆,是天一觀的道長(zhǎng)征炼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)躬贡,這世上最難降的妖魔是什么谆奥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮拂玻,結(jié)果婚禮上酸些,老公的妹妹穿的比我還像新娘。我一直安慰自己檐蚜,他們只是感情好魄懂,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著闯第,像睡著了一般市栗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天填帽,我揣著相機(jī)與錄音蛛淋,去河邊找鬼。 笑死盲赊,一個(gè)胖子當(dāng)著我的面吹牛铣鹏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哀蘑,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼诚卸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了绘迁?” 一聲冷哼從身側(cè)響起合溺,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缀台,沒(méi)想到半個(gè)月后棠赛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膛腐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年睛约,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哲身。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辩涝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勘天,到底是詐尸還是另有隱情怔揩,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布脯丝,位于F島的核電站商膊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宠进。R本人自食惡果不足惜晕拆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砰苍。 院中可真熱鬧潦匈,春花似錦、人聲如沸赚导。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吼旧。三九已至,卻和暖如春未舟,著一層夾襖步出監(jiān)牢的瞬間圈暗,已是汗流浹背掂为。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留员串,地道東北人勇哗。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像寸齐,于是被迫代替她去往敵國(guó)和親欲诺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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