Android App 架構設計

簡介

本文是對谷歌原生文檔的翻譯,僅供學習參照。

原文鏈接

此文檔寫給希望學習最優編程實踐和架構以開發健壯、高質量APP的開發者。

開發者常遇到的問題

傳統的桌面程序大多數使用場景是有一個啓動入口,作爲一個獨立進程運行。Android app結構要複雜很多,一個典型的Android app由很多組件構成,包括activities,fragment,services,content providers 和broadcast receivers。

App四大組件在Androidmanifest.xml文件裏面聲明,它們被安卓系統用來決定如何構建App在設備上的交互體驗。之前提到,桌面App一般運行在一個獨立進程裏面,安卓App則不同。安卓設備交互場景經常會遇到多個App之間切換任務,因此安卓App設計上需要靈活一些以滿足需求。

舉個例子:使用社交App分享一張照片。首先,社交App發intent啓動相機App,此時用戶已經離開了社交App,但是用戶可能並未感知到這個狀態。相機App又可能發送intent啓動其他的App,比如圖片選擇器,最終用戶返回社交App完成分享照片的動作。在此過程還可能被其他事件中斷,比如來電話,用戶需要等通話結束以後纔可以繼續操作分享照片的動作。

Android app組件可以被單獨啓動,也可以無序啓動,並且可能會隨時被用戶手動或系統銷燬。用戶無法掌控Android app組件的生命週期,因此不應該在組件裏面存儲app的數據和狀態,組件之間也不應相互依賴耦合。

通用的架構原則

  1. 關注點分離一個常見的錯誤是將所有代碼都寫到Activity或者Fragment,這麼做不僅會讓代碼看起來很臃腫,難以閱讀和維護,而且容易導致生命週期相關的問題產生。按照谷歌官方的開發推薦,任何不是處理UI和系統的代碼都不應該寫到這兩個類裏面。Activity或者Fragment可能會因爲一些原因被系統銷燬,比如低內存的時候,用戶無法掌控。爲了使得App更加的穩定可靠,我們應該在開發中最小化對它們的依賴。
  2. Mode驅動UI更新:優選持久化模型。持久化模型有兩個好處:(1)當app被系統回收的時候用戶不用再擔心丟失數據,即使網絡不通,app仍然可以繼續運行。Modes是一種組件,它用來持有app的數據,它獨立於views和app的其他組件。因此,它與app四大組件存在的生命週期問題是隔離的。保持UI代碼和邏輯之間的隔離可以使得代碼更加容易管理和維護。通過引入Modes類,並給予每一個mode定義好明確的數據映射關係,可以使得app更加方便測試和維護。

推薦的app架構

本節通過一個案例介紹如何使用Architecture Components 來構建App。

說明:理論上,不存在一種萬能架構使得app在所有場景下都是最優。因此,本文推薦的架構適用範圍有限,如果你已經有不錯的架構方式,可以不用更換。

下面我們開始案例,假設需要構建UI來顯示用戶的簡歷,簡歷數據需要通過REST API從後臺獲取。

構建接口

UI 對應的類UserProfileFragment.java 佈局文件是 user_profile_layout.xml.

爲了驅動UI,model需要持有兩個數據元素

  • The User ID: 用戶ID。傳遞這個數值最好的方式是在fragment的argument裏面。因爲如果app進程被系統回收,這個數值會被持久化,當app重啓的時候還可以獲取到這個數據。
  • The User object: A POJO 持有用戶數據.

給予ViewModel類構建UserProfileViewModel

ViewModel 用來爲UI組件(activity或者fragment)提供數據,並且負責UI與業務數據處理之間的通信。ViewMode不關心UI的變化,例如activity旋轉或者重建。

Now we have 3 files.

  • user_profile.xml: UI定義
  • UserProfileViewModel.java: 爲UI提供數據
  • UserProfileFragment.java: UI控制器,用來顯示UserProfileViewModel提供的數據

以下是代碼實現(爲簡單起見,佈局文件被省略)

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

Note: 上面的例子使用 LifecycleFragment 代替 Fragment 。等lifecycles API穩定以後,Support Library裏面的Fragment將會更新實現 LifecycleOwner

現在已經有三個模塊,如何連接他們?當ViewModel裏面用戶數據更新以後,需要通知UI來同步顯示。這時LiveData登場了。

LiveData 可觀察的數據持有者。它允許app組件在不創建與它之間顯示和剛性依賴的前提下觀察LiveData對象的改變。LiveData遵守app組件的生命週期原則,可以避免內存泄漏。

Note: 如果你正在使用其他的庫,例如RxJava 或者 Agera,可以不用替換成LiveData。但是如果你準備使用LiveData,你務必要正確處置生命週期,這樣當LifecycleOwner stopped的時候你的數據流也暫停,當LifecycleOwner destroyed的時候你的數據流也destroyed。如果你要在使用LiveData的時候搭配RxJava2等庫,可以通過引入 android.arch.lifecycle:reactivestreams

現在我們使用LiveData來替換 UserProfileViewModel裏面User的屬性,這樣當這個值有變化會通知fragment同步更新。LiveData遵守lifecycle原則,當它不在被需要的時候會自動清理引用。

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

Now we modify UserProfileFragment to observe the data and update the UI.

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

每當用戶數據更新,onChanged回調方法會被執行用來刷新UI。

如果你熟悉其它類似LiveData功能的庫,你會發現我們沒有重寫fragment的onStop()方法來停止觀察數據。使用LiveData無需做這個處理,因爲它被設計自動感知Lifecycle,當fragment執行onDestroy()的時候,LiveData會自動刪除觀察。

We also didn’t do anything special to handle configuration changes (for example, user rotating the screen). The ViewModel is automatically restored when the configuration changes, so as soon as the new fragment comes to life, it will receive the same instance of ViewModel and the callback will be called instantly with the current data. This is the reason why ViewModels should not reference Views directly; they can outlive the View’s lifecycle. SeeThe lifecycle of a ViewModel.

不必針對configuration 的改變做特殊處理(例如activity旋轉)。當configuration 變化時,ViewModel 會自動恢復數據。因此,當fragment重新啓動,將獲取到與configuration 變化之前相同的ViewModels ,並且callback會被馬上調用,數據也和之前保持一致。因此,ViewModes不必直接引用Views,他們可以超越View的生命週期。參考The lifecycle of a ViewModel.

獲取數據

至此,ViewModel和fragment之間已經建立了聯繫,那麼ViewModel如何獲取用戶數據的?在本例中,我們假設後臺提供的是REST API,我們使用Retrofit 來封裝http請求。

下圖展示了使用retrofit與後臺交互

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

ViewMode可以直接調用webservice來從後臺獲取數據並傳遞給用戶對象,這是最簡單的一種實現方式。不過這不是最佳方案,因爲隨着業務增加,這種架構會比較難擴展和維護。這種架構給予ViewMode過多的責任,因此違背了上文提到的關注分離原則。另外,ViewModel已經跟Activity或者Fragment的生命週期綁定,當UI的生命週期結束時數據丟失是非常不好的體驗,因此我們引入了Repository模塊,將ViewModel獲取數據的工作交於它。

Repository 模塊用來處理數據,包括:從哪兒獲取數據,當數據變化時調用什麼API來更新。它可以被看做是不同數據源之間的調解人,數據來源大致有:持久化數據,webservice,緩存等。

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

respository看起來不是必須的,但是它有一個很重要的優點:抽象了app獲取數據的通道,比如在上例中ViewModel並不知道數據源來自Webservice,因此當我們業務需要變更時可以方便修改數據源。

Note: 爲了簡單起見,我們已經排除了網絡錯誤案例。 暴露錯誤和加載狀態的替代實現請參閱Addendum: exposing network status.

管理組件之間的依賴:

UserRepository獲取數據的時候需要一個webservice實例,創建webservice實例並不麻煩,但是需要知道構造webservice時的依賴。這樣會稍顯複雜併產生冗餘代碼,因爲並不是只有UserrRepository需要webservice的實例,其他類在使用webservice實例的時候都需要知道構建webservice時的依賴。

有兩個模式可以用來解決上述問題:

  • 依賴注入: 依賴注入框架允許你定義一個類的依賴,而不必自己去構建這個依賴對象。代碼執行期,有專門的類來負責提供依賴對象。我們推薦Android app使用谷歌Dagger 2 框架來實現依賴注入。Dagger 2通過遍歷依賴關係樹自動構建對象,並在依賴關係上提供編譯時保證。
  • 服務定位: 服務定位器提供了一個註冊表,其中類可以獲取它們的依賴關係而不是構造它們. 它的實現比依賴注入簡單很多,如果你對依賴注入不熟悉,可以考慮使用服務定位。

這些模式允許您擴展代碼,因爲它們提供明確的模式來管理依賴關係,而不會重複代碼或增加複雜性。 兩者都允許交換實現進行測試; 這是使用它們的主要好處之一。

在本示例中,我們繼續使用Dagger 2 來管理依賴關係。

連接 ViewModel 與 repository

我們通過修改 UserProfileViewModel 來使用repository

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

緩存數據

repository對於抽象webservice的請求非常奏效,但是上文示例只有一個數據源,所以可能感覺不是很明顯。

UserRepository也有自身的缺陷,如果用戶離開了UserProfileFragment,app會重新加載數據。這有兩個弊端:

  1. 浪費了網絡流量
  2. 重新請求網絡數據耗費時間,用戶需要等待

爲此,我們在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會立即重新顯示之前的數據,這是因爲我們使用了內存緩存。但是當用戶離開app,進程被殺死,然後再次返回app,此時會出現什麼情況?

在當前架構中,遇到這種情況需要重新從後臺讀取數據。這個體驗不太好,既耽誤時間也浪費流量。爲了解決這個問題,可以緩存web請求。但是 如果相同的用戶數據從另一種類型的請求顯示(例如,獲取一個朋友列表)會發生什麼情況? 那麼您的應用程序可能會顯示不一致的數據,這是最令人困惑的用戶體驗。 例如,相同的用戶的數據可能會不同,因爲朋友列表請求和用戶請求可以在不同的時間執行。 您的應用需要合併,以避免顯示不一致的數據。

解決上面問題最好的方法是使用持久化模型。再次谷歌推薦使用Room。

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

Note: 如果您熟悉SQLite ORM或Realm等不同數據庫的其他持久性解決方案,則無需將其替換爲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

現在我們需要通過一種方式來向數據庫插入用戶數據,爲此我們先新建一個data access object (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,只會在至少有一個主動觀察者時更新數據。

Note: 從alpha 1版本開始,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與數據分離是很好的做法,因爲改變UI的原因可能有很多。

有兩種方法來解決這種情況遇到的問題:

  • 改變getUser的實現,返回一個LiveData數據,包含網絡操作的狀態。這裏有一個參考示例Addendum: exposing network status
  • 在repository類中新增一個public方法,返回用戶對象最新的狀態。如果希望通過在UI上顯示網絡狀態來響應用戶動作(例如下拉刷新),那麼此方法更好。

唯一的可靠數據源

不同REST API返回相同數據也很正常,例如:如果後臺有另外一個請求接口返回一個朋友列表,同樣的用戶對象可能會來自兩個不同的請求接口。通過webservice獲取數據,當後臺數據在在多次請求之間發生變化時,用戶得到的數據可能會出現不一致的現象。因此,在UserRepository實現中web service的回調只是將數據存儲到數據庫,然後數據庫發生改變會觸發生成一個激活的LiveData對象。

在這個模型中,數據庫是唯一可靠的數據來源,app其他組件通過repository訪問數據庫。無論是否使用磁盤緩存,我們推薦repository來爲app設計唯一一個可靠的數據源。

測試

上面提到,關注分離帶來的一個好處是方便測試。來看下如何測試每一個模塊

  • User Interface & Interactions: 這是唯一需要Android UI Instrumentation測試的。 測試UI代碼的最好方法是創建一個Espresso測試。 您可以創建該fragment併爲其提供一個模擬的ViewModel。 由於fragment只與ViewModel進行通信,所以模擬它將足以完全測試UI。

  • ViewModel: ViewModel 可以使用 JUnit test測試.

  • UserRepository: 您也可以使用JUnit測試來測試UserRepository。 您需要模擬Webservice和DAO。 您可以測試它是否進行正確的Web服務調用,將結果保存到數據庫中,如果數據被緩存並且是最新的,則不會發生任何不必要的請求。 既然Webservice和UserDao都是接口,那麼你可以模擬它們,或爲更復雜的測試用例僞造一個實現。

  • UserDao: 測試DAO類的推薦方法是使用儀器測試。 由於這些儀器測試不需要任何UI,因此它們可以快速運行。 對於每個測試,可以創建一個內存數據庫,以確保測試沒有任何副作用(如更改磁盤上的數據庫文件)。

    Room 還允許指定數據庫實現,以便您可以通過向其提供支持SQLiteOpenHelper的JUnit實現來測試它。 通常不推薦使用此方法,因爲在設備上運行的SQLite版本可能與主機上的SQLite版本不同。

  • Webservice: 保證測試與外界的獨立性很重要,即使是webservice測試也應該避免向後臺發送網絡請求。有很多的庫可以幫助來實現這個需求,例如MockWebServer 可以僞造一個本地服務器來用於測試。

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

    • InstantTaskExecutorRule: 此規則可用於強制架構組件立即執行調用線程上的任何後臺操作。
    • CountingTaskExecutorRule: 該規則可用於儀器測試,以等待架構組件的後臺操作或將其連接到Espresso作爲閒置資源。

最終的架構

下圖展示了谷歌推薦的架構包含的所有模塊,以及模塊之間如何交互。

img

指導原則


編程是一項創造性工作,開發Android應用程序也不例外。 無論是在多個activity或fragment之間傳遞數據,檢索遠程數據並將其在本地保持離線模式,還是任何其他場景,都有多種方法來解決問題,

谷歌推薦,遵循這些建議將使您的代碼庫從長遠來看更加強大,可測試和可維護。

  • 安卓四大組件不應當被用作數據源
  • 關注分離,爲應用程序的各個模塊之間創建明確的責任界限
  • 模塊內部高內聚,儘量少的暴露每個模塊的是實現細節
  • 模塊之間低耦合
  • 不重複造輪子,將開發精力聚焦在自己app獨一無二的特性上
  • 持久化數據,這樣用戶離線狀態也可以使用
  • 爲repository設計使用唯一的數據源 Single source of truth.

附錄:暴露網絡狀態


在上面推薦的應用程序體系結構部分,我們故意省略網絡錯誤和加載狀態,以保持樣本簡單。 在本節中,我們演示了一種使用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);
    }
}

因爲在從磁盤中顯示它的時候加載數據是一個常見的用例,所以我們要創建一個幫助類,可以在多個地方重複使用NetworkBoundResourcethat。 以下是NetworkBoundResource的決策樹:

img

請求從監聽數據庫開始,當第一次從數據庫加載數據,NetworkBoundResource會檢查數據是否有效,如果有效則分發出去,否則開始從網絡獲取數據。注意,這兩個動作可以同時發生,例如你在發送網絡請求的時候可能想先展示數據庫中的緩存數據,等網絡請求完成再用來更新數據內容。

如果網絡請求成功完成,則將響應保存到數據庫中並重新初始化流。 如果網絡請求失敗,我們直接發送失敗。

以下是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
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

請注意,上面的類定義了兩個類型參數(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();
    }
}

現在,我們可以使用NetworkBoundResource將我們的磁盤和網絡綁定用戶實現寫入存儲庫。

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();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章