LiveData + ViewModel + Room (Google 官文)+Demo

本指南適用于那些過去構(gòu)建應(yīng)用程序有基礎(chǔ)知識派阱,現(xiàn)在想知道構(gòu)建強大的生產(chǎn)質(zhì)量應(yīng)用程序最佳實踐和建議的體系結(jié)構(gòu)的開發(fā)人員偎巢。

注意:本指南假設(shè)讀者熟悉Android框架爽茴。如果您不熟悉應(yīng)用程序開發(fā)咆霜,請查看入門培訓(xùn)系列籍铁,其中包含本指南的必備主題狐树。

APP開發(fā)者面臨的常見問題

與傳統(tǒng)的桌面應(yīng)用程序不同涮母,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多挟冠,在大多數(shù)情況下鹏漆,它們只有一個啟動快捷方式的入口點巩梢,并且可以作為一個單一的整體進程運行。一個典型的Android應(yīng)用程序是由多個應(yīng)用程序組件構(gòu)成的艺玲,包括活動括蝠,片段,服務(wù)饭聚,內(nèi)容提供者和廣播接收器忌警。

大多數(shù)這些應(yīng)用程序組件都是在Android操作系統(tǒng)使用的應(yīng)用程序清單中聲明的??,清單決定如何將您的應(yīng)用程序與其設(shè)備的整體用戶體驗的集成若治。如前所述慨蓝,桌面應(yīng)用程序傳統(tǒng)上是以整體的方式運行感混,但正確的Android應(yīng)用程序編寫需要更加靈活,因為用戶可以在設(shè)備上的不同應(yīng)用程序不斷切換流程和任務(wù)礼烈。

例如弧满,請考慮在您最喜愛的社交網(wǎng)絡(luò)應(yīng)用程序中分享照片時會發(fā)生什么情況。該應(yīng)用程序觸發(fā)Android操作系統(tǒng)此熬,啟動相機應(yīng)用程序來處理請求的相機意圖庭呜。此時,用戶離開了社交網(wǎng)絡(luò)應(yīng)用犀忱,但他們的體驗是無縫的募谎。相機應(yīng)用程序又可能觸發(fā)其他意圖,例如啟動文件選擇器阴汇,該文件選擇器可以啟動另一個應(yīng)用程序数冬。最終用戶回到社交網(wǎng)絡(luò)應(yīng)用程序并分享照片。此外搀庶,用戶在這個過程的任何時候都可能被電話打斷拐纱,并在打完電話后回來分享照片。

在Android中哥倔,這種應(yīng)用程序跳轉(zhuǎn)行為很常見秸架,所以您的應(yīng)用程序必須正確處理這些流程。請記住咆蒿,移動設(shè)備是資源受限东抹,所以在任何時候,操作系統(tǒng)可能需要殺死一些應(yīng)用程序沃测,以騰出空間給新的的應(yīng)用或進程缭黔。所有這一切的關(guān)鍵是,您的應(yīng)用程序組件可以單獨和無序地啟動芽突,并可以在任何時候由用戶或系統(tǒng)銷毀试浙。由于應(yīng)用程序組件是短暫的董瞻,它們的生命周期(創(chuàng)建和銷毀時)不在您的控制之下寞蚌,因此您不應(yīng)該在應(yīng)用程序組件中存儲任何應(yīng)用程序數(shù)據(jù)或狀態(tài),并且應(yīng)用程序組件不應(yīng)相互依賴钠糊。

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

如果您不能使用應(yīng)用程序組件來存儲應(yīng)用程序數(shù)據(jù)和狀態(tài)挟秤,應(yīng)該如何構(gòu)建應(yīng)用程序?

你應(yīng)該關(guān)注的最重要的事情是在你的應(yīng)用程序中抄伍,將所有代碼寫入Activity或Fragment是一個常見的錯誤艘刚。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類中。盡可能保持精簡可以避免許多生命周期相關(guān)的問題截珍。記住攀甚,你不擁有這些類箩朴,它們只是體現(xiàn)操作系統(tǒng)和你的應(yīng)用程序之間的契約的粘合劑。 Android操作系統(tǒng)可能會隨時根據(jù)用戶交互或其他因素(如低內(nèi)存)來銷毀它們秋度。最好最大限度地減少對他們的依賴炸庞,以提供可靠的用戶體驗。

第二個重要的原則是你應(yīng)該從一個模型驅(qū)動(數(shù)據(jù))你的UI荚斯,最好是一個持久模型埠居。持久性是理想的,原因有兩個:如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源事期,則您的用戶不會丟失數(shù)據(jù)滥壕,即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,您的應(yīng)用程序也將繼續(xù)工作兽泣。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件绎橘。它們獨立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問題是隔離的唠倦。保持簡單的UI代碼和減少的應(yīng)用程序邏輯金踪,使管理更容易。將您的應(yīng)用程序放在具有明確定義的管理數(shù)據(jù)的模型類上牵敷,使它們可測試胡岔,并能使您的應(yīng)用程序保持一致。

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

在本節(jié)中枷餐,我們將演示如何通過使用用例來構(gòu)建使用體系結(jié)構(gòu)組件的應(yīng)用程序靶瘸。

注意:不可能有一種編寫應(yīng)用程序的方法,對每種情況都是最好的毛肋。也就是說怨咪,這個推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個很好的起點。如果您已經(jīng)有了編寫Android應(yīng)用的好方法润匙,則不需要更改诗眨。想象一下,我們正在構(gòu)建一個顯示用戶配置文件的用戶界面孕讳。該用戶配置文件將使用REST API從我們自己的后端獲取數(shù)據(jù)匠楚。

假如,我們正在構(gòu)建一個顯示用戶信息的用戶界面厂财。該用戶信息將使用REST API從我們自己的后端獲取芋簿。

構(gòu)建用戶界面

用戶界面將由fragment UserProfileFragment.java 和 布局文件user_profile_layout.xml組成。

為了驅(qū)動用戶界面璃饱,我們的數(shù)據(jù)模型需要保存兩個數(shù)據(jù)元素与斤。

  • User ID:用戶的標(biāo)識符。最好使用Fragment參數(shù)將此信息傳遞到Fragment中。如果Android操作系統(tǒng)破壞您的進程撩穿,這些信息將被保留磷支,以便在您的應(yīng)用下次重新啟動時可用。

  • User對象:持有用戶數(shù)據(jù)的POJO

我們將創(chuàng)建一個基于ViewModel類的UserProfileViewModel來保存這些信息食寡。

ViewModel為特定的UI組件(如Fragment或Activity)提供數(shù)據(jù)齐唆,并處理與數(shù)據(jù)處理業(yè)務(wù)部分的通信,例如調(diào)用其他組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改冻河。 ViewModel不知道UI箍邮,并且不受配置更改的影響,例如由于旋轉(zhuǎn)而重新創(chuàng)建Activity叨叙。

現(xiàn)在我們有3個文件:

  • user_profile.xml

  • UserProfileViewModel.java

  • UserProfileFragment.java

下面是我們的開始的實現(xià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 Fragment {
    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);
    }
}

現(xiàn)在锭弊,我們有這三個代碼模塊,我們?nèi)绾芜B接它們擂错?畢竟味滞,當(dāng)ViewModel的用戶字段被設(shè)置,我們需要一種方式來通知用戶界面钮呀。這是使用LiveData類的地方剑鞍。


LiveData是一個可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData對象的更改爽醋,而不會在它們之間創(chuàng)建明確的和嚴(yán)格的依賴關(guān)系路徑蚁署。 LiveData還尊重您的應(yīng)用程序組件(活動,片段蚂四,服務(wù))的生命周期狀態(tài)光戈,并做正確的事情來防止對象泄漏,使您的應(yīng)用程序不消耗更多的內(nèi)存遂赠。


注意:如果您已經(jīng)在使用類似RxJava或Agera的庫久妆,則可以繼續(xù)使用它們而不是LiveData。但是跷睦,當(dāng)您使用它們或其他方法時筷弦,請確保正確處理生命周期,以便在相關(guān)的LifecycleOwner停止時停止數(shù)據(jù)流抑诸,并在銷毀LifecycleOwner時銷毀數(shù)據(jù)流烂琴。您還可以添加android.arch.lifecycle:reactivestreams工件以將LiveData與另一個反應(yīng)流庫(例如RxJava2)一起使用。

現(xiàn)在我們將UserProfileViewModel中的User字段替換為一個LiveData <User>哼鬓,以便在數(shù)據(jù)更新時通知這個分段监右。 LiveData最棒的地方在于它具有生命周期感知能力,當(dāng)不再需要引用時會自動清除引用异希。

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

現(xiàn)在我們修改UserProfileFragment來觀察數(shù)據(jù)并更新UI。

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

每次更新用戶數(shù)據(jù)時,都會調(diào)用onChanged回調(diào)称簿,并刷新UI扣癣。

如果您熟悉使用可觀察回調(diào)的其他庫,您可能已經(jīng)意識到憨降,我們不必重寫片段的onStop()方法來停止觀察數(shù)據(jù)父虑。這對于LiveData來說并不是必須的绣夺,因為它是生命周期感知的舆逃,這意味著它不會調(diào)用回調(diào),除非片段處于活動狀態(tài)(收到onStart()师倔,但沒有收到onStop())悔叽。當(dāng)片段收到onDestroy()時莱衩,LiveData也會自動移除觀察者。

我們也沒有做任何特殊的處理配置變化(例如娇澎,用戶旋轉(zhuǎn)屏幕)笨蚁。當(dāng)配置改變時,ViewModel會自動恢復(fù)趟庄,所以一旦新的片段生效括细,它將接收到同一個ViewModel的實例,回調(diào)將被立即調(diào)用當(dāng)前數(shù)據(jù)戚啥。這就是ViewModel不能直接引用Views的原因奋单。他們可以超越View的生命周期。請參閱ViewModel的生命周期猫十。

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

現(xiàn)在我們已經(jīng)將ViewModel連接到了片段辱匿,但是ViewModel如何獲取用戶數(shù)據(jù)呢?在這個例子中炫彩,我們假設(shè)我們的后端提供了一個REST API匾七。我們將使用Retrofit庫來訪問我們的后端,盡管您可以自由使用不同的庫來達到同樣的目的江兢。

這里是我們的改進的Web服務(wù)與我們的后端進行通信:

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

ViewModel的一個原始的實現(xiàn)可以直接調(diào)用Web服務(wù)來獲取數(shù)據(jù)并將其分配給用戶對象昨忆。即使它可行,您的應(yīng)用程序也將難以維持杉允。它給ViewModel類提供了太多的責(zé)任邑贴,這違背了前面提到的分離原則。此外叔磷,ViewModel的范圍與活動或片段生命周期相關(guān)聯(lián)拢驾,因此,在生命周期結(jié)束時丟失所有數(shù)據(jù)是不好的用戶體驗改基。相反繁疤,我們的ViewModel將這個工作委托給一個新的Repository模塊。


Repository模塊負(fù)責(zé)處理數(shù)據(jù)操作。他們提供了一個干凈的API到應(yīng)用程序的其余部分稠腊。他們知道從何處獲取數(shù)據(jù)以及在更新數(shù)據(jù)時調(diào)用哪些API躁染。您可以將它們視為不同數(shù)據(jù)源(持久模型,Web服務(wù)架忌,緩存等)之間的中介吞彤。


下面的UserRepository類使用WebService來獲取用戶數(shù)據(jù)項:

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

即使Repository模塊看起來沒有必要,它也有著重要的作用叹放。它從應(yīng)用程序的其余部分提取數(shù)據(jù)源∈嗡。現(xiàn)在我們的ViewModel不知道數(shù)據(jù)是由Webservice獲取的,這意味著我們可以根據(jù)需要將其交換為其他實現(xiàn)井仰。

注意:為簡單起見埋嵌,我們忽略了網(wǎng)絡(luò)錯誤的情況。有關(guān)公開錯誤和加載狀態(tài)的替代實現(xiàn)糕档,請參閱附錄:公開網(wǎng)絡(luò)狀態(tài)莉恼。

管理組件之間的依賴關(guān)系:

上面的UserRepository類需要Webservice的一個實例來完成它的工作。它可以簡單地創(chuàng)建它速那,但要做到這一點俐银,它也需要知道Webservice類的依賴關(guān)系來構(gòu)造它。這會使代碼復(fù)雜化和重復(fù)(例如端仰,每個需要Webservice實例的類都需要知道如何用它的依賴關(guān)系來構(gòu)造它)捶惜。此外,UserRepository可能不是唯一需要Web服務(wù)的類荔烧。如果每個類創(chuàng)建一個新的WebService吱七,這將是非常資源沉重。

有兩種模式可以用來解決這個問題:

  • 依賴注入:依賴注入允許類在不構(gòu)造它們的情況下定義它們的依賴關(guān)系鹤竭。在運行時踊餐,另一個類負(fù)責(zé)提供這些依賴關(guān)系。我們推薦Google的Dagger 2庫在Android應(yīng)用程序中實現(xiàn)依賴注入臀稚。 Dagger 2通過遍歷依賴關(guān)系樹來自動構(gòu)造對象吝岭,并為依賴關(guān)系提供編譯時間保證。

  • 服務(wù)定位器:服務(wù)定位器提供了一個注冊表吧寺,類可以獲得它們的依賴而不是構(gòu)建它們窜管。實現(xiàn)起來比依賴注入(DI)更容易,所以如果你不熟悉DI稚机,可以使用Service Locator幕帆。

這些模式允許您擴展您的代碼,因為它們提供了用于管理依賴關(guān)系的清晰模式赖条,無需重復(fù)代碼或增加復(fù)雜性失乾。他們兩人也允許交換實現(xiàn)測試;這是使用它們的主要好處之一

在這個例子中常熙,我們將使用Dagger 2來管理依賴關(guān)系。

連接ViewModel和存儲庫

現(xiàn)在我們修改我們的UserProfileViewModel來使用倉庫仗扬。

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

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

上面的存儲庫實現(xiàn)對抽象調(diào)用Web服務(wù)是有好處的症概,但是因為它只依賴于一個數(shù)據(jù)源蕾额,所以它不是很有用早芭。

上面的UserRepository實現(xiàn)的問題是,在獲取數(shù)據(jù)之后诅蝶,它不保留在任何地方退个。如果用戶離開UserProfileFragment并返回到該應(yīng)用程序,則應(yīng)用程序會重新獲取數(shù)據(jù)调炬。這是不好的语盈,原因有兩個:浪費寶貴的網(wǎng)絡(luò)帶寬并強制用戶等待新的查詢完成。為了解決這個問題缰泡,我們將添加一個新的數(shù)據(jù)源到我們的UserRepository中刀荒,它將把用戶對象緩存在內(nèi)存中。

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

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

在我們當(dāng)前的實現(xiàn)中棘钞,如果用戶旋轉(zhuǎn)屏幕或離開并返回到應(yīng)用程序缠借,則現(xiàn)有UI將立即可見,因為存儲庫從內(nèi)存中緩存中檢索數(shù)據(jù)宜猜。但是泼返,如果用戶離開應(yīng)用程序,并在Android操作系統(tǒng)殺死該進程后數(shù)小時后回來姨拥,會發(fā)生什么绅喉?

在目前的實施中,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)叫乌。這不僅是一個糟糕的用戶體驗柴罐,而且會浪費,因為它會使用移動數(shù)據(jù)重新獲取相同的數(shù)據(jù)憨奸。您可以簡單地通過緩存Web請求來解決這個問題革屠,但是會產(chǎn)生新的問題。如果相同的用戶數(shù)據(jù)顯示來自另一種類型的請求(例如膀藐,獲取朋友列表)屠阻,會發(fā)生什么情況?那么你的應(yīng)用程序可能會顯示不一致的數(shù)據(jù)额各,這是一個混亂的用戶體驗充其量国觉。例如,由于好友列表請求和用戶請求可以在不同的時間執(zhí)行虾啦,所以相同用戶的數(shù)據(jù)可能會以不同的方式顯示麻诀。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)痕寓。

處理這個問題的正確方法是使用持久模型。這是Room持久性庫優(yōu)點的地方蝇闭。

Room是一個對象映射庫呻率,提供本地數(shù)據(jù)持久性和最小的樣板代碼。在編譯時呻引,它會根據(jù)模式驗證每個查詢礼仗,以便斷開的SQL查詢導(dǎo)致編譯時錯誤,而不是運行時失敗逻悠。Room抽象出一些使用原始SQL表和查詢的底層實現(xiàn)細(xì)節(jié)元践。它還允許觀察對數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改,通過LiveData對象公開這些更改童谒。另外单旁,它明確定義了解決常見問題的線程約束,例如訪問主線程上的存儲饥伊。

注意:如果您的應(yīng)用程序已經(jīng)使用另一個持久性解決方案(如SQLite對象關(guān)系映射(ORM))象浑,則不需要使用Room替換現(xiàn)有的解決方案。但是琅豆,如果您正在編寫新的應(yīng)用程序或重構(gòu)現(xiàn)有應(yīng)用程序愉豺,我們建議使用Room來保存應(yīng)用程序的數(shù)據(jù)。這樣趋距,您可以利用庫的抽象和查詢驗證功能粒氧。

要使用Room,我們需要定義我們的本地模式节腐。首先外盯,使用@Entity注釋User類,將其標(biāo)記為數(shù)據(jù)庫中的表翼雀。

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

然后饱苟,通過為您的應(yīng)用程序擴展RoomDatabase來創(chuàng)建一個數(shù)據(jù)庫類:

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

注意MyDatabase是抽象的。Room自動提供一個實施狼渊。有關(guān)詳細(xì)信息箱熬,請參見Room文檔

現(xiàn)在我們需要一種將用戶數(shù)據(jù)插入數(shù)據(jù)庫的方法。為此狈邑,我們將創(chuàng)建一個數(shù)據(jù)訪問對象(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ù)庫類中引用DAO米苹。

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

注意load方法返回一個LiveData <User>糕伐。Room知道數(shù)據(jù)庫何時被修改,當(dāng)數(shù)據(jù)改變時它會自動通知所有活動的觀察者蘸嘶。因為它使用的是LiveData良瞧,所以這將是有效的陪汽,因為它只會在至少有一個活動觀察者的情況下更新數(shù)據(jù)。

注意:Room根據(jù)表格修改檢查失效褥蚯,這意味著它可能發(fā)送誤報通知挚冤。

現(xiàn)在我們可以修改我們的UserRepository以合并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);
        // return a LiveData directly from the database.
        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());
            }
        });
    }
}

請注意赞庶,即使我們更改了UserRepository中數(shù)據(jù)的來源训挡,我們也不需要更改UserProfileViewModel或UserProfileFragment。這是抽象提供的靈活性尘执。這對于測試也很好舍哄,因為在測試UserProfileViewModel的時候可以提供一個偽造的UserRepository宴凉。

現(xiàn)在我們的代碼是完整的誊锭。如果用戶以后回到相同的用戶界面,他們會立即看到用戶信息弥锄,因為我們持久化了丧靡。同時,如果數(shù)據(jù)陳舊籽暇,我們的倉庫將在后臺更新數(shù)據(jù)温治。當(dāng)然,根據(jù)您的使用情況戒悠,如果數(shù)據(jù)太舊熬荆,您可能不希望顯示持久數(shù)據(jù)。

在一些使用情況下绸狐,如下拉刷新卤恳,UI顯示用戶是否正在進行網(wǎng)絡(luò)操作是非常重要的。將UI操作與實際數(shù)據(jù)分開是一種很好的做法寒矿,因為它可能因各種原因而更新(例如突琳,如果我們獲取朋友列表,則可能會再次觸發(fā)同一用戶符相,觸發(fā)LiveData <User>更新)拆融。從用戶界面的角度來看,一個正在進行中請求只是另一個數(shù)據(jù)點啊终,類似于其他任何數(shù)據(jù)(如用戶對象)镜豹。

這個用例有兩個常見的解決方案:

  • 更改getUser以返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。附錄中提供了一個示例實現(xiàn):公開網(wǎng)絡(luò)狀態(tài)部分蓝牲。

  • 在存儲庫類中提供另一個可以返回用戶刷新狀態(tài)的公共函數(shù)趟脂。如果只想響應(yīng)顯式的用戶操作(如下拉刷新)來顯示網(wǎng)絡(luò)狀態(tài),則此選項更好搞旭。

單一的事實來源

不同的REST API端點通常返回相同的數(shù)據(jù)散怖。例如菇绵,如果我們的后端擁有另一個返回朋友列表的端點,則同一個用戶對象可能來自兩個不同的API端點镇眷,也許端點不同咬最。如果UserRepository原樣返回來自Webservice請求的響應(yīng),那么我們的UI可能會顯示不一致的數(shù)據(jù)欠动,因為這些請求之間的數(shù)據(jù)可能在服務(wù)器端發(fā)生更改永乌。這就是為什么在UserRepository實現(xiàn)中,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫中具伍。然后翅雏,對數(shù)據(jù)庫的更改將觸發(fā)活動LiveData對象上的回調(diào)。

在這個模型中人芽,數(shù)據(jù)庫充當(dāng)真實數(shù)據(jù)的單一來源望几,應(yīng)用程序的其他部分通過存儲庫訪問它。即使您使用磁盤緩存萤厅,都建議您將存儲庫數(shù)據(jù)源指定為應(yīng)用程序其余部分的單一來源橄抹。

測試

我們已經(jīng)提到分離的好處之一就是可測試性。讓我們看看我們?nèi)绾螠y試每個代碼模塊惕味。

  • 用戶界面:這將是您唯一需要Android UI Instrumentation測試的時間楼誓。測試UI代碼的最好方法是創(chuàng)建一個Espresso測試。您可以創(chuàng)建片段并為其提供一個模擬的ViewModel名挥。由于該片段只與ViewModel交談疟羹,因此模擬它將足以完全測試此UI。

  • ViewModel: ViewModel可以使用JUnit測試進行測試禀倔。你只需要模擬UserRepository來測試它榄融。

  • UserRepository:您也可以使用JUnit測試來測試UserRepository。你需要模擬Web服務(wù)和DAO剃袍。您可以測試它是否進行正確的Web服務(wù)調(diào)用捎谨,將結(jié)果保存到數(shù)據(jù)庫中,如果數(shù)據(jù)已緩存且最新涛救,則不會發(fā)出任何不必要的請求。既然Webservice和UserDao都是接口舒萎,你可以模擬它們或者為更復(fù)雜的測試用例創(chuàng)建假實現(xiàn)。

  • UserDao: 測試DAO類的推薦方法是使用儀器測試臂寝。由于這些測試不需要任何用戶界面,他們?nèi)匀粫\行得很快咆贬。對于每個測試,您可以創(chuàng)建一個內(nèi)存數(shù)據(jù)庫掏缎,以確保測試沒有任何副作用(如更改磁盤上的數(shù)據(jù)庫文件)。Room還允許指定數(shù)據(jù)庫實現(xiàn)眷蜈,以便通過提供SupportSQLiteOpenHelper的JUnit實現(xiàn)來測試它沪哺。通常不建議使用這種方法,因為設(shè)備上運行的SQLite版本可能與主機上的SQLite版本不同酌儒。

  • Webservice:使測試獨立于外界是很重要的辜妓,所以即使是Web服務(wù)測試也應(yīng)該避免對后端進行網(wǎng)絡(luò)調(diào)用。有很多l(xiāng)ibrary可以幫助你今豆。例如嫌拣,MockWebServer是一個偉大的庫,可以幫助您為測試創(chuàng)建一個假的本地服務(wù)器呆躲。

  • Testing Artifacts: 架構(gòu)組件提供了一個maven工件來控制其后臺線程。在android.arch.core:核心測試工件內(nèi)部捶索,有2個JUnit規(guī)則:

    • InstantTaskExecutorRule:此規(guī)則可用于強制架構(gòu)組件立即在調(diào)用線程上執(zhí)行任何后臺操作插掂。
    • CountingTaskExecutorRule:此規(guī)則可用于檢測測試,以等待體系結(jié)構(gòu)組件的后臺操作或?qū)⑵渥鳛殚e置資源連接到Espresso腥例。

架構(gòu)圖

下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>

1495481828442840.png

指導(dǎo)原則

編程是一個創(chuàng)造性的領(lǐng)域辅甥,構(gòu)建Android應(yīng)用程序也不是一個例外。解決問題的方法有很多種燎竖,可以在多個活動或片段之間傳遞數(shù)據(jù)璃弄,檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進行脫機模式,也可以使用許多其他常見應(yīng)用程序遇到的情況构回。

盡管以下建議不是強制性的夏块,但是我們的經(jīng)驗是,遵循這些建議將使您的代碼基礎(chǔ)更加健壯纤掸,可測試和可維護脐供。

  • 您在清單中定義的入口點(活動,服務(wù)借跪,廣播接收器等)不是數(shù)據(jù)的來源政己。相反,他們只應(yīng)該協(xié)調(diào)與該入口點相關(guān)的數(shù)據(jù)子集歇由。由于每個應(yīng)用程序組件的壽命相當(dāng)短沦泌,這取決于用戶與設(shè)備的交互以及運行時的整體當(dāng)前運行狀況赦肃,因此您不希望這些入口點中的任何一個成為數(shù)據(jù)源他宛。

  • 在應(yīng)用程序的各個模塊之間創(chuàng)建明確界定的責(zé)任厅各。例如队塘,不要將從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼跨代碼庫中的多個類或包傳播遮怜。同樣锯梁,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個類中陌凳。

  • 盡可能少地從每個模塊公開合敦。不要試圖創(chuàng)建“only one”的快捷方式充岛,從一個模塊公開內(nèi)部實現(xiàn)細(xì)節(jié)裸准。您可能在短期內(nèi)獲得一些時間炒俱,但隨著您的代碼庫的發(fā)展砸王,您將多次支付技術(shù)債務(wù)谦铃。

  • 在定義模塊之間的交互時驹闰,請考慮如何使每個模塊獨立地進行測試。例如诵肛,如果有一個定義良好的API從網(wǎng)絡(luò)中獲取數(shù)據(jù)褪秀,將會更容易測試將數(shù)據(jù)保存在本地數(shù)據(jù)庫中的模塊媒吗。相反蝴猪,如果將這兩個模塊的邏輯混合在一起,或者在整個代碼庫中撒上網(wǎng)絡(luò)代碼米酬,那么要測試就更加困難了赃额。

  • 你的應(yīng)用程序的核心是什么讓它從其他中脫穎而出跳芳。不要花費時間重復(fù)發(fā)明輪子飞盆,或者一次又一次地寫出相同的樣板代碼孽水。相反女气,將精力集中在讓您的應(yīng)用獨一無二的東西上炼鞠,讓Android架構(gòu)組件和其他推薦的庫處理重復(fù)的樣板。

  • 堅持盡可能多的相關(guān)和新鮮的數(shù)據(jù)软吐,以便您的應(yīng)用程序在設(shè)備處于離線模式時可用姿现。雖然您可以享受持續(xù)高速的連接备典,但用戶可能不會提佣。

  • 您的存儲庫應(yīng)該指定一個數(shù)據(jù)源作為單一的事實來源拌屏。無論何時您的應(yīng)用程序需要訪問這些數(shù)據(jù)倚喂,都應(yīng)始終從單一的事實源頭開始端圈。有關(guān)更多信息,請參閱單一來源的真相仑嗅。

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

在上面推薦的應(yīng)用程序體系結(jié)構(gòu)部分,我們故意省略網(wǎng)絡(luò)錯誤和加載狀態(tài)吠冤,以保持樣本簡單拯辙。在本節(jié)中涯保,我們將演示如何使用Resource類公開網(wǎng)絡(luò)狀態(tài)來封裝數(shù)據(jù)及其狀態(tài)夕春。

以下是一個示例實現(xiàn):

//a generic class that describes a data with a status
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);
    }
}

因為從磁盤加載數(shù)據(jù)的時候是一個常見的用例,所以我們要創(chuàng)建一個可以在多個地方重復(fù)使用的幫助類NetworkBoundResource速侈。以下是NetworkBoundResource的決策樹:

1495482897135330.png

它通過觀察資源的數(shù)據(jù)庫開始。當(dāng)?shù)谝淮螐臄?shù)據(jù)庫加載條目時每界,NetworkBoundResource檢查結(jié)果是否足夠好以便分派和/或從網(wǎng)絡(luò)中獲取眨层。請注意谐岁,這兩種情況可能同時發(fā)生,因為您可能希望在從網(wǎng)絡(luò)更新緩存數(shù)據(jù)時顯示緩存的數(shù)據(jù)沛善。

如果網(wǎng)絡(luò)呼叫成功完成金刁,則將響應(yīng)保存到數(shù)據(jù)庫中并重新初始化流媳友。如果網(wǎng)絡(luò)請求失敗醇锚,我們直接發(fā)送失敗焊唬。

注意:在將新數(shù)據(jù)保存到磁盤之后赶促,我們會重新初始化數(shù)據(jù)庫中的數(shù)據(jù)流鸥滨,但通常我們不需要這樣做婿滓,因為數(shù)據(jù)庫將分派更改。另一方面容客,依靠數(shù)據(jù)庫來調(diào)度變化將依賴于不好的副作用但两,因為如果數(shù)據(jù)沒有變化谨湘,數(shù)據(jù)庫可以避免調(diào)度變化紧阔,那么它可能會中斷擅耽。我們也不希望發(fā)送從網(wǎng)絡(luò)到達的結(jié)果乖仇,因為這將違背單一的事實來源(也許在數(shù)據(jù)庫中有觸發(fā)器會改變保存的值)乃沙。如果沒有新的數(shù)據(jù)警儒,我們也不想派遣SUCCESS冷蚂,因為它會向客戶發(fā)送錯誤的信息蝙茶。

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

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be
    // fetched from the network.
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    @MainThread
    protected void onFetchFailed() {
    }

    // returns a LiveData that represents the resource, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

請注意钳恕,上面的類定義了兩個類型參數(shù)(ResultType忧额,RequestType)睦番,因為從API返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配托嚣。

另外請注意示启,上面的代碼使用ApiResponse進行網(wǎng)絡(luò)請求夫嗓。 ApiResponse是Retrofit2.Call類的一個簡單的包裝舍咖,將其響應(yīng)轉(zhuǎn)換為LiveData谎仲。

以下是NetworkBoundResource類的其余實現(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();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        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) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

現(xiàn)在杉武,我們可以使用NetworkBoundResource將我們的磁盤和網(wǎng)絡(luò)綁定的User實現(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)容合作請聯(lián)系作者
  • 序言:七十年代末较店,一起剝皮案震驚了整個濱河市梁呈,隨后出現(xiàn)的幾起案子官卡,更是在濱河造成了極大的恐慌寻咒,老刑警劉巖毛秘,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異霞揉,居然都是意外死亡晰骑,警方通過查閱死者的電腦和手機秽荞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門扬跋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凌节,“玉大人,你說我怎么就攤上這事垒棋〉鸺埽” “怎么了乖订?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜡吧。 經(jīng)常有香客問我占键,道長君仆,這世上最難降的妖魔是什么返咱? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任咖摹,我火速辦了婚禮,結(jié)果婚禮上店读,老公的妹妹穿的比我還像新娘屯断。我一直安慰自己殖演,他們只是感情好剃氧,可當(dāng)我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布朋鞍。 她就那樣靜靜地躺著,像睡著了一般坎吻。 火紅的嫁衣襯著肌膚如雪宇葱。 梳的紋絲不亂的頭發(fā)上诸尽,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天印颤,我揣著相機與錄音,去河邊找鬼年局。 笑死际看,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的矢否。 我是一名探鬼主播仲闽,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼僵朗!你這毒婦竟也來了赖欣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤衣迷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挑社,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡阱当,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡渤刃,死狀恐怖洋闽,靈堂內(nèi)的尸體忽然破棺而出刊懈,到底是詐尸還是另有隱情,我是刑警寧澤将谊,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響谷丸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一樟凄、第九天 我趴在偏房一處隱蔽的房頂上張望服鹅。 院中可真熱鬧仗哨,春花似錦苇倡、人聲如沸综慎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至船庇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漓藕。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工份名, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丧没,地道東北人往声。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓妻顶,卻偏偏與公主長得像叛氨,于是被迫代替她去往敵國和親饭冬。 傳聞我的和親對象是個殘疾皇子执虹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,922評論 2 361

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