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

本指南適用於那些過去構建應用程序有基礎知識,如今想知道構建強大的生產質量應用程序最佳實踐和建議的體系結構的開發人員。android

注意:本指南假設讀者熟悉Android框架。若是您不熟悉應用程序開發,請查看入門培訓系列,其中包含本指南的必備主題。git

APP開發者面臨的常見問題

與傳統的桌面應用程序不一樣,Android應用程序的結構要複雜得多,在大多數狀況下,它們只有一個啓動快捷方式的入口點,而且能夠做爲一個單一的總體進程運行。一個典型的Android應用程序是由多個應用程序組件構成的,包括活動,片斷,服務,內容提供者和廣播接收器。github

大多數這些應用程序組件都是在Android操做系統使用的應用程序清單中聲明的​​,清單決定如何將您的應用程序與其設備的總體用戶體驗的集成。如前所述,桌面應用程序傳統上是以總體的方式運行,但正確的Android應用程序編寫須要更加靈活,由於用戶能夠在設備上的不一樣應用程序不斷切換流程和任務。web

例如,請考慮在您最喜好的社交網絡應用程序中分享照片時會發生什麼狀況。該應用程序觸發Android操做系統,啓動相機應用程序來處理請求的相機意圖。此時,用戶離開了社交網絡應用,但他們的體驗是無縫的。相機應用程序又可能觸發其餘意圖,例如啓動文件選擇器,該文件選擇器能夠啓動另外一個應用程序。最終用戶回到社交網絡應用程序並分享照片。此外,用戶在這個過程的任什麼時候候均可能被電話打斷,並在打完電話後回來分享照片。數據庫

在Android中,這種應用程序跳轉行爲很常見,因此您的應用程序必須正確處理這些流程。請記住,移動設備是資源受限,因此在任什麼時候候,操做系統可能須要殺死一些應用程序,以騰出空間給新的的應用或進程。全部這一切的關鍵是,您的應用程序組件能夠單獨和無序地啓動,並能夠在任什麼時候候由用戶或系統銷燬。因爲應用程序組件是短暫的,它們的生命週期(建立和銷燬時)不在您的控制之下,所以您不該該在應用程序組件中存儲任何應用程序數據或狀態,而且應用程序組件不該相互依賴。編程

常見的架構原則

若是您不能使用應用程序組件來存儲應用程序數據和狀態,應該如何構建應用程序?後端

你應該關注的最重要的事情是在你的應用程序中,將全部代碼寫入Activity或Fragment是一個常見的錯誤。任何不處理UI或操做系統交互的代碼都不該該在這些類中。儘量保持精簡能夠避免許多生命週期相關的問題。記住,你不擁有這些類,它們只是體現操做系統和你的應用程序之間的契約的粘合劑。 Android操做系統可能會隨時根據用戶交互或其餘因素(如低內存)來銷燬它們。最好最大限度地減小對他們的依賴,以提供可靠的用戶體驗。api

第二個重要的原則是你應該從一個模型驅動(數據)你的UI,最好是一個持久模型。持久性是理想的,緣由有兩個:若是操做系統破壞您的應用程序以釋放資源,則您的用戶不會丟失數據,即便網絡鏈接不穩定或鏈接不上,您的應用程序也將繼續工做。模型是負責處理應用程序數據的組件。它們獨立於應用程序中的視圖和應用程序組件,所以它們與這些組件的生命週期問題是隔離的。保持簡單的UI代碼和減小的應用程序邏輯,使管理更容易。將您的應用程序放在具備明肯定義的管理數據的模型類上,使它們可測試,並能使您的應用程序保持一致。

推薦的應用架構

在本節中,咱們將演示如何經過使用用例來構建使用體系結構組件的應用程序。

注意:不可能有一種編寫應用程序的方法,對每種狀況都是最好的。也就是說,這個推薦的架構應該是大多數用例的一個很好的起點。若是您已經有了編寫Android應用的好方法,則不須要更改。想象一下,咱們正在構建一個顯示用戶配置文件的用戶界面。該用戶配置文件將使用REST API從咱們本身的後端獲取數據。

假如,咱們正在構建一個顯示用戶信息的用戶界面。該用戶信息將使用REST API從咱們本身的後端獲取。

構建用戶界面

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

爲了驅動用戶界面,咱們的數據模型須要保存兩個數據元素。

  • User ID:用戶的標識符。最好使用Fragment參數將此信息傳遞到Fragment中。若是Android操做系統破壞您的進程,這些信息將被保留,以便在您的應用下次從新啓動時可用。

  • User對象:持有用戶數據的POJO

咱們將建立一個基於ViewModel類的UserProfileViewModel來保存這些信息。

ViewModel爲特定的UI組件(如Fragment或Activity)提供數據,並處理與數據處理業務部分的通訊,例如調用其餘組件來加載數據或轉發用戶修改。 ViewModel不知道UI,而且不受配置更改的影響,例如因爲旋轉而從新建立Activity。

如今咱們有3個文件:

  • user_profile.xml

  • UserProfileViewModel.java

  • UserProfileFragment.java

下面是咱們的開始的實現(佈局文件爲簡單起見被省略):

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);
    }
}複製代碼

如今,咱們有這三個代碼模塊,咱們如何鏈接它們?畢竟,當ViewModel的用戶字段被設置,咱們須要一種方式來通知用戶界面。這是使用LiveData類的地方。


LiveData是一個可觀察的數據持有者。它容許應用程序中的組件觀察LiveData對象的更改,而不會在它們之間建立明確的和嚴格的依賴關係路徑。 LiveData還尊重您的應用程序組件(活動,片斷,服務)的生命週期狀態,並作正確的事情來防止對象泄漏,使您的應用程序不消耗更多的內存。


注意:若是您已經在使用相似RxJava或Agera的庫,則能夠繼續使用它們而不是LiveData。可是,當您使用它們或其餘方法時,請確保正確處理生命週期,以便在相關的LifecycleOwner中止時中止數據流,並在銷燬LifecycleOwner時銷燬數據流。您還能夠添加android.arch.lifecycle:reactivestreams工件以將LiveData與另外一個反應流庫(例如RxJava2)一塊兒使用。

如今咱們將UserProfileViewModel中的User字段替換爲一個LiveData ,以便在數據更新時通知這個分段。 LiveData最棒的地方在於它具備生命週期感知能力,當再也不須要引用時會自動清除引用。

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

如今咱們修改UserProfileFragment來觀察數據並更新UI。

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

每次更新用戶數據時,都會調用onChanged回調,並刷新UI。

若是您熟悉使用可觀察回調的其餘庫,您可能已經意識到,咱們沒必要重寫片斷的onStop()方法來中止觀察數據。這對於LiveData來講並非必須的,由於它是生命週期感知的,這意味着它不會調用回調,除非片斷處於活動狀態(收到onStart(),但沒有收到onStop())。當片斷收到onDestroy()時,LiveData也會自動移除觀察者。

咱們也沒有作任何特殊的處理配置變化(例如,用戶旋轉屏幕)。當配置改變時,ViewModel會自動恢復,因此一旦新的片斷生效,它將接收到同一個ViewModel的實例,回調將被當即調用當前數據。這就是ViewModel不能直接引用Views的緣由。他們能夠超越View的生命週期。請參閱ViewModel的生命週期。

獲取數據

如今咱們已經將ViewModel鏈接到了片斷,可是ViewModel如何獲取用戶數據呢?在這個例子中,咱們假設咱們的後端提供了一個REST API。咱們將使用Retrofit庫來訪問咱們的後端,儘管您能夠自由使用不一樣的庫來達到一樣的目的。

這裏是咱們的改進的Web服務與咱們的後端進行通訊:

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的一個原始的實現能夠直接調用Web服務來獲取數據並將其分配給用戶對象。即便它可行,您的應用程序也將難以維持。它給ViewModel類提供了太多的責任,這違背了前面提到的分離原則。此外,ViewModel的範圍與活動或片斷生命週期相關聯,所以,在生命週期結束時丟失全部數據是很差的用戶體驗。相反,咱們的ViewModel將這個工做委託給一個新的Repository模塊。


Repository模塊負責處理數據操做。他們提供了一個乾淨的API到應用程序的其他部分。他們知道從何處獲取數據以及在更新數據時調用哪些API。您能夠將它們視爲不一樣數據源(持久模型,Web服務,緩存等)之間的中介。


下面的UserRepository類使用WebService來獲取用戶數據項:

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模塊看起來沒有必要,它也有着重要的做用。它從應用程序的其他部分提取數據源。如今咱們的ViewModel不知道數據是由Webservice獲取的,這意味着咱們能夠根據須要將其交換爲其餘實現。

注意:爲簡單起見,咱們忽略了網絡錯誤的狀況。有關公開錯誤和加載狀態的替代實現,請參閱附錄:公開網絡狀態。

管理組件之間的依賴關係:

上面的UserRepository類須要Webservice的一個實例來完成它的工做。它能夠簡單地建立它,但要作到這一點,它也須要知道Webservice類的依賴關係來構造它。這會使代碼複雜化和重複(例如,每一個須要Webservice實例的類都須要知道如何用它的依賴關係來構造它)。此外,UserRepository可能不是惟一須要Web服務的類。若是每一個類建立一個新的WebService,這將是很是資源沉重。

有兩種模式能夠用來解決這個問題:

  • 依賴注入:依賴注入容許類在不構造它們的狀況下定義它們的依賴關係。在運行時,另外一個類負責提供這些依賴關係。咱們推薦Google的Dagger 2庫在Android應用程序中實現依賴注入。 Dagger 2經過遍歷依賴關係樹來自動構造對象,併爲依賴關係提供編譯時間保證。

  • 服務定位器:服務定位器提供了一個註冊表,類能夠得到它們的依賴而不是構建它們。實現起來比依賴注入(DI)更容易,因此若是你不熟悉DI,可使用Service Locator。

這些模式容許您擴展您的代碼,由於它們提供了用於管理依賴關係的清晰模式,無需重複代碼或增長複雜性。他們兩人也容許交換實現測試;這是使用它們的主要好處之一

在這個例子中,咱們將使用Dagger 2來管理依賴關係。

鏈接ViewModel和存儲庫

如今咱們修改咱們的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; } }複製代碼

緩存數據

上面的存儲庫實現對抽象調用Web服務是有好處的,可是由於它只依賴於一個數據源,因此它不是頗有用。

上面的UserRepository實現的問題是,在獲取數據以後,它不保留在任何地方。若是用戶離開UserProfileFragment並返回到該應用程序,則應用程序會從新獲取數據。這是很差的,緣由有兩個:浪費寶貴的網絡帶寬並強制用戶等待新的查詢完成。爲了解決這個問題,咱們將添加一個新的數據源到咱們的UserRepository中,它將把用戶對象緩存在內存中。

@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;
    }
}複製代碼

持久化數據

在咱們當前的實現中,若是用戶旋轉屏幕或離開並返回到應用程序,則現有UI將當即可見,由於存儲庫從內存中緩存中檢索數據。可是,若是用戶離開應用程序,並在Android操做系統殺死該進程後數小時後回來,會發生什麼?

在目前的實施中,咱們將須要從網絡上從新獲取數據。這不只是一個糟糕的用戶體驗,並且會浪費,由於它會使用移動數據從新獲取相同的數據。您能夠簡單地經過緩存Web請求來解決這個問題,可是會產生新的問題。若是相同的用戶數據顯示來自另外一種類型的請求(例如,獲取朋友列表),會發生什麼狀況?那麼你的應用程序可能會顯示不一致的數據,這是一個混亂的用戶體驗充其量。例如,因爲好友列表請求和用戶請求能夠在不一樣的時間執行,因此相同用戶的數據可能會以不一樣的方式顯示。您的應用須要合併它們以免顯示不一致的數據。

處理這個問題的正確方法是使用持久模型。這是Room持久性庫優勢的地方。

Room是一個對象映射庫,提供本地數據持久性和最小的樣板代碼。在編譯時,它會根據模式驗證每一個查詢,以便斷開的SQL查詢致使編譯時錯誤,而不是運行時失敗。Room抽象出一些使用原始SQL表和查詢的底層實現細節。它還容許觀察對數據庫數據(包括集合和鏈接查詢)的更改,經過LiveData對象公開這些更改。另外,它明肯定義瞭解決常見問題的線程約束,例如訪問主線程上的存儲。

注意:若是您的應用程序已經使用另外一個持久性解決方案(如SQLite對象關係映射(ORM)),則不須要使用Room替換現有的解決方案。可是,若是您正在編寫新的應用程序或重構現有應用程序,咱們建議使用Room來保存應用程序的數據。這樣,您能夠利用庫的抽象和查詢驗證功能。

要使用Room,咱們須要定義咱們的本地模式。首先,使用@Entity註釋User類,將其標記爲數據庫中的表。

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

而後,經過爲您的應用程序擴展RoomDatabase來建立一個數據庫類:

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

注意MyDatabase是抽象的。Room自動提供一個實施。有關詳細信息,請參見Room文檔

如今咱們須要一種將用戶數據插入數據庫的方法。爲此,咱們將建立一個數據訪問對象(DAO)。

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

而後,從咱們的數據庫類中引用DAO。

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

注意load方法返回一個LiveData 。Room知道數據庫什麼時候被修改,當數據改變時它會自動通知全部活動的觀察者。由於它使用的是LiveData,因此這將是有效的,由於它只會在至少有一個活動觀察者的狀況下更新數據。

注意:Room根據表格修改檢查失效,這意味着它可能發送誤報通知。

如今咱們能夠修改咱們的UserRepository以合併Room數據源。

@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中數據的來源,咱們也不須要更改UserProfileViewModel或UserProfileFragment。這是抽象提供的靈活性。這對於測試也很好,由於在測試UserProfileViewModel的時候能夠提供一個僞造的UserRepository。

如今咱們的代碼是完整的。若是用戶之後回到相同的用戶界面,他們會當即看到用戶信息,由於咱們持久化了。同時,若是數據陳舊,咱們的倉庫將在後臺更新數據。固然,根據您的使用狀況,若是數據太舊,您可能不但願顯示持久數據。

在一些使用狀況下,以下拉刷新,UI顯示用戶是否正在進行網絡操做是很是重要的。將UI操做與實際數據分開是一種很好的作法,由於它可能因各類緣由而更新(例如,若是咱們獲取朋友列表,則可能會再次觸發同一用戶,觸發LiveData 更新)。從用戶界面的角度來看,一個正在進行中請求只是另外一個數據點,相似於其餘任何數據(如用戶對象)。

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

  • 更改getUser以返回包含網絡操做狀態的LiveData。附錄中提供了一個示例實現:公開網絡狀態部分。

  • 在存儲庫類中提供另外一個能夠返回用戶刷新狀態的公共函數。若是隻想響應顯式的用戶操做(以下拉刷新)來顯示網絡狀態,則此選項更好。

單一的事實來源

不一樣的REST API端點一般返回相同的數據。例如,若是咱們的後端擁有另外一個返回朋友列表的端點,則同一個用戶對象可能來自兩個不一樣的API端點,也許端點不一樣。若是UserRepository原樣返回來自Webservice請求的響應,那麼咱們的UI可能會顯示不一致的數據,由於這些請求之間的數據可能在服務器端發生更改。這就是爲何在UserRepository實現中,Web服務回調只是將數據保存到數據庫中。而後,對數據庫的更改將觸發活動LiveData對象上的回調。

在這個模型中,數據庫充當真實數據的單一來源,應用程序的其餘部分經過存儲庫訪問它。即便您使用磁盤緩存,都建議您將存儲庫數據源指定爲應用程序其他部分的單一來源。

測試

咱們已經提到分離的好處之一就是可測試性。讓咱們看看咱們如何測試每一個代碼模塊。

  • 用戶界面:這將是您惟一須要Android UI Instrumentation測試的時間。測試UI代碼的最好方法是建立一個Espresso測試。您能夠建立片斷併爲其提供一個模擬的ViewModel。因爲該片斷只與ViewModel交談,所以模擬它將足以徹底測試此UI。

  • ViewModel: ViewModel可使用JUnit測試進行測試。你只須要模擬UserRepository來測試它。

  • UserRepository:您也可使用JUnit測試來測試UserRepository。你須要模擬Web服務和DAO。您能夠測試它是否進行正確的Web服務調用,將結果保存到數據庫中,若是數據已緩存且最新,則不會發出任何沒必要要的請求。既然Webservice和UserDao都是接口,你能夠模擬它們或者爲更復雜的測試用例建立假實現。

  • UserDao: 測試DAO類的推薦方法是使用儀器測試。因爲這些測試不須要任何用戶界面,他們仍然會運行得很快。對於每一個測試,您能夠建立一個內存數據庫,以確保測試沒有任何反作用(如更改磁盤上的數據庫文件)。Room還容許指定數據庫實現,以便經過提供SupportSQLiteOpenHelper的JUnit實現來測試它。一般不建議使用這種方法,由於設備上運行的SQLite版本可能與主機上的SQLite版本不一樣。

  • Webservice:使測試獨立於外界是很重要的,因此即便是Web服務測試也應該避免對後端進行網絡調用。有不少library能夠幫助你。例如,MockWebServer是一個偉大的庫,能夠幫助您爲測試建立一個假的本地服務器。

  • Testing Artifacts: 架構組件提供了一個maven工件來控制其後臺線程。在android.arch.core:核心測試工件內部,有2個JUnit規則:

    • InstantTaskExecutorRule:此規則可用於強制架構組件當即在調用線程上執行任何後臺操做。
    • CountingTaskExecutorRule:此規則可用於檢測測試,以等待體系結構組件的後臺操做或將其做爲閒置資源鏈接到Espresso。

架構圖

下圖顯示了咱們推薦的體系結構中的全部模塊以及它們如何相互交互:

1495481828442840.png
1495481828442840.png

指導原則

編程是一個創造性的領域,構建Android應用程序也不是一個例外。解決問題的方法有不少種,能夠在多個活動或片斷之間傳遞數據,檢索遠程數據並將其保存在本地以進行脫機模式,也可使用許多其餘常見應用程序遇到的狀況。

儘管如下建議不是強制性的,可是咱們的經驗是,遵循這些建議將使您的代碼基礎更加健壯,可測試和可維護。

  • 您在清單中定義的入口點(活動,服務,廣播接收器等)不是數據的來源。相反,他們只應該協調與該入口點相關的數據子集。因爲每一個應用程序組件的壽命至關短,這取決於用戶與設備的交互以及運行時的總體當前運行情況,所以您不但願這些入口點中的任何一個成爲數據源。

  • 在應用程序的各個模塊之間建立明確界定的責任。例如,不要將從網絡加載數據的代碼跨代碼庫中的多個類或包傳播。一樣,不要把不相關的職責 - 好比數據緩存和數據綁定 - 放到同一個類中。

  • 儘量少地從每一個模塊公開。不要試圖建立「only one」的快捷方式,從一個模塊公開內部實現細節。您可能在短時間內得到一些時間,但隨着您的代碼庫的發展,您將屢次支付技術債務。

  • 在定義模塊之間的交互時,請考慮如何使每一個模塊獨立地進行測試。例如,若是有一個定義良好的API從網絡中獲取數據,將會更容易測試將數據保存在本地數據庫中的模塊。相反,若是將這兩個模塊的邏輯混合在一塊兒,或者在整個代碼庫中撒上網絡代碼,那麼要測試就更加困難了。

  • 你的應用程序的核心是什麼讓它從其餘中脫穎而出。不要花費時間重複發明輪子,或者一次又一次地寫出相同的樣板代碼。相反,將精力集中在讓您的應用獨一無二的東西上,讓Android架構組件和其餘推薦的庫處理重複的樣板。

  • 堅持儘量多的相關和新鮮的數據,以便您的應用程序在設備處於離線模式時可用。雖然您能夠享受持續高速的鏈接,但用戶可能不會。

  • 您的存儲庫應該指定一個數據源做爲單一的事實來源。不管什麼時候您的應用程序須要訪問這些數據,都應始終從單一的事實源頭開始。有關更多信息,請參閱單一來源的真相。

附錄:網絡狀態

在上面推薦的應用程序體系結構部分,咱們故意省略網絡錯誤和加載狀態,以保持樣本簡單。在本節中,咱們將演示如何使用Resource類公開網絡狀態來封裝數據及其狀態。

如下是一個示例實現:

//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);
    }
}複製代碼

由於從磁盤加載數據的時候是一個常見的用例,因此咱們要建立一個能夠在多個地方重複使用的幫助類NetworkBoundResource。如下是NetworkBoundResource的決策樹:

1495482897135330.png
1495482897135330.png

它經過觀察資源的數據庫開始。當第一次從數據庫加載條目時,NetworkBoundResource檢查結果是否足夠好以便分派和/或從網絡中獲取。請注意,這兩種狀況可能同時發生,由於您可能但願在從網絡更新緩存數據時顯示緩存的數據。

若是網絡呼叫成功完成,則將響應保存到數據庫中並從新初始化流。若是網絡請求失敗,咱們直接發送失敗。

注意:在將新數據保存到磁盤以後,咱們會從新初始化數據庫中的數據流,但一般咱們不須要這樣作,由於數據庫將分派更改。另外一方面,依靠數據庫來調度變化將依賴於很差的反作用,由於若是數據沒有變化,數據庫能夠避免調度變化,那麼它可能會中斷。咱們也不但願發送從網絡到達的結果,由於這將違背單一的事實來源(也許在數據庫中有觸發器會改變保存的值)。若是沒有新的數據,咱們也不想派遣SUCCESS,由於它會向客戶發送錯誤的信息。

如下是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();
}複製代碼

請注意,上面的類定義了兩個類型參數(ResultType,RequestType),由於從API返回的數據類型可能與本地使用的數據類型不匹配。

另外請注意,上面的代碼使用ApiResponse進行網絡請求。 ApiResponse是Retrofit2.Call類的一個簡單的包裝,將其響應轉換爲LiveData。

如下是NetworkBoundResource類的其他實現:

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;
    }
}複製代碼

如今,咱們可使用NetworkBoundResource將咱們的磁盤和網絡綁定的User實現寫入到存儲庫中。

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();
    }
}複製代碼
相關文章
相關標籤/搜索