【翻譯】Architecture Components - Android App應用架構組件指南

應用架構組件指南

原文請看:https://developer.android.com/topic/libraries/architecture/guide.html

本指南適用于具有構建安卓APP基礎知識的開發人員,現在想知道最佳實踐和建議的架構,以構建強大的生產級安卓APP。

注意:這篇文章假設你已經熟悉安卓框架,如果你是一個開發新手,請先閱讀安卓開發入門指南

安卓開發者面臨的常見問題


與傳統的桌面應用程序不同,在大多數情況下,啟動快捷方式都有一個入口點,并作為一個單獨的進程運行,安卓APP的應用程序架構更為復雜,一個典型的安卓APP由多個APP組件構成,包含activities、fragments、services、content providers and broadcast receivers。

所有的這些APP組件都定義在AndroidMenifest.xml文件中,被整合到安卓操作系統作為設備整體的用戶體驗。然而像之前所說的,傳統的桌面應用是作為一個整理的單獨的進程運行,但是正確編寫的安卓APP需要更靈活,因為用戶通過設備上的不同APP不斷切換流程和任務。

例如:思考下在你喜歡的社交網絡APP中分享照片時會發生什么。APP會希望使用照相機,安卓操作系統將會啟動拍照APP來完成這個請求。在這點上,用戶離開了社交網絡APP,但是這個體驗是無縫的。拍照APP可能也會有其他的請求,比如啟動文件選擇器APP,最終用戶會回到社交網絡APP中分享照片。此外,在用戶操作的任何時候都可能被電話打斷,而且可以打完電話后繼續分享照片。

在安卓中,這種應用程序之間相互切換的行為很常見,因此你必須正確的處理這些流程。請記住,移動設備資源有限,所以在任何時候,操作系統都有可能會殺死一些APP,為新的APP騰出資源。

所有的應用程序組件都可以單獨或者無序啟動,并且可在任意時候被用戶或者系統銷毀。因為應用程序組件是短暫的,并且它們的生命周期不受你的控制,所以不應該將任何應用程序數據和狀態存存儲在應用程序組件中,并且應用程序組件不應該相互依賴。

常見的架構原理


如果你不能在應用程序組件中存儲數據和狀態,那么該如何做呢?

你應該關注的最重要的問題是在你的APP中如何做好隔離,將所有代碼都寫在Activity或者Fragment是一個常見的問題。任何不處理UI或者與操作系統交互的代碼都不應該寫在這些類中,讓他們保持最可能的干凈可以避免許多與生命周期相關的問題。不要忘記你不能完全控制他們,他們只是一些操作系統和你APP的約定的合約類。在任何時候安卓操作系統可能會根據用戶的交互或者其他類似低內存等因素來銷毀他們。最好盡量減少對他們的依賴以提供統計堅固的體驗。

第二個重要的原則是你應該通過model來驅動UI,最好是持久化的model。為什么要是持久化model有兩個原因:如果操作系統因為低內存銷毀你的APP來釋放資源,你的用戶將不會丟失數據,即使是網絡也連接不上你的APP也還能繼續運行。model是負責處理應用程序組件數據的組件,他們獨立于APP的視圖(Views)和應用程序組件,所以他們能夠避免有生命周期的問題,保持UI代碼盡量簡單,使得代碼更容易管理維護。將數據放在model類中,并明確定義好其管理數據的責任,這樣可以使得APP更容易測試,并保持一致性。

推薦的APP架構


在本節中,我們將用一個例子來演示如何使用Architecture Components來構建一個APP。

注意:最佳的方案不可能適用所有場景,話雖如此,本章節推薦的架構對于大多數案例應該是個不錯的選擇,如果你已經有了自己一套方法,并且非常適用,你可以不用更改,繼續使用你的方法。

假設我們正常制作一個顯示用戶資料的UI。用戶資料會從一個REST API來獲取。

創建UI界面

UI界面包含一個fragment UserProfileFragment.java和它對應的layout文件user_profile_layout.xml。

為了構造這個UI,我們的數據model需要包含兩個元素。

  • 用戶ID,用戶ID標示,使用fragment參數的方式傳遞到fragment中,如果安卓操作系統銷毀了當前進程,下次APP啟動的時候,這個用戶ID會被保留下來。

  • User對象,包含用戶資料信息的POJO對象。

我們會創建一個基于ViewModel類型的UserProfileViewModel對象來保存上面的信息。

ViewModel為特定的UI組件提供數據,比如:Activity、Fragment等,并處理與業務部門的通訊,比如調用其他組件加載數據。ViewModel與View之間相互隔離,并且不受activity屏幕旋轉導致activity重新創建的影響。

現在我們有三個文件了。

  • user_profile.xml: layout文件。
  • UserProfileViewModel.java: 為UI提供數據的viewmodel對象。
  • UserProfileFragment.java: UI控制器,用來顯示ViewModel中的數據,還有處理用戶和UI的交互。

以下是我們的初始實現(簡單起見,我們先忽略layout文件)

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 的lifecycles API變穩定后,Android Support Library的Fragment class會實現LifecycleOwner接口。

現在我們有三個代碼模塊,我們怎么把他們關聯起來?畢竟,當ViewModel的用戶字段被設置時,我們需要通知到UI,所以我們現在需要一個新的對象LiveData。

LiveData是一個可觀察的數據持有者,他可以讓APP中的組件觀察LiveData是否發生改變,而且不需要他們之前有嚴格的相互依賴關系。LiveData還會遵循應用程序組件(Activity,Fragment, Service)的生命周期狀態來避免內存泄露,從而是你的APP不會消費太多的內存。

注意:如果你已經在使用像RxJava或者Agera這類的類庫,你可以繼續使用他們。但是請確認自己需要正確的處理生命周期。你還可以添加android.arch.lifecycle:reactivestreams artifact,將LiveData與RxJava結合起來使用。

現在我們把UserProfileViewModel的User字段替換成LiveData<User>,這樣的話,當user被更新時,fragment可以被通知到。LiveData能正確的處理生命周期,所以它會自動清理references,當它已經不需要時。

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

現在我們修改UserProfileFragment來觀察user發生變化時更新UI。

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

每當user字段被更新后,onChanged回調就會被調用,然后UI就會被更新。

如果你非常熟悉其他的觀察回調模式的話,你會發現我們沒有覆蓋fragment的onStop()方法來停止觀察數據。這個是不必須的,因為LiveData是生命周期感知的。這意味著除非fragment是激活狀態(onStart但是還沒有onStop),要不然是不會發起回調的。當fragment調用onStop后, LiveData還會自動刪除觀察者。

我們沒有做任何事情來處理configuration changes(當用戶旋轉屏幕)。當發生configuration changes事件,新的fragment被產生時,ViewModel會自動被恢復。新的fragment會關聯上之前ViewModel的相同實例,LiveData會被瞬間回調。這就是為什么ViewModel不應該直接關聯到View,他們應該獨立于View的生命周期。請查看:The lifecycle of a ViewModel

獲取數據

現在我們已經關聯了fragment和ViewModel,但是ViewModel如何獲取用戶數據呢?在這個例子中,我們假設后端提供了一個REST接口。我們將會使用Retrofit庫來調用接口獲取數據,如果你使用另外的庫來調用接口也沒關系。

下面是調用接口的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);
}

ViewModel的簡單實現可以直接調用Webservice來獲取數據并對user對象賦值,盡管這樣可以,但是隨著你的APP代碼量越來越多你的代碼將會越來越難以維護。ViewModel承擔了太多的責任,這也違背了我們之前提的分離法則。此外,ViewModel綁定到了Activity或者Fragment的生命周期,所以如果他們生命周期結束了丟失所有的數據是一個糟糕的用戶體驗,所以,我們的ViewModel會將獲取數據這個工作委托給一個新的Repository模塊。

Repository模塊負責處理數據操作,他們為APP剩下的部分提供了一個干凈的API。他們知道從哪里獲取數據,知道數據有更新時調用什么API。你可以把他們看成是不同數據源(持久化模型,web service,緩存等)之間的中介者。

下面的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模塊不太重要,但是它確實起了很大的作用,它抽象了data source這層,現在ViewModel不知道數據是通過WebService獲取的,這樣就以為著我們可以替換這個實現。

注意:為了簡單起見,我們忽略了處理相關的網絡錯誤等邏輯。有關這塊的內容請看:Addendum: exposing network status 最后一章

組件之間依賴配置(IOC)

上面的UserRepository類需要Webservice的一個實例,你可以簡單的直接創建這個類,但是你還需要知道Webservice的依賴。這將會顯著的復雜化代碼。(例如,需要Webservice實例的每個類將需要知道如何使用它的依賴來構造它)。此外,UserRepository可能不是唯一需要Webservice的類,如果每個類都創建一個WebService,這會非常耗費資源。

這里有兩個方案可以解決這個問題:

  • Dependency Injection:依賴注入允許類定義他們的依賴關系而不必創建他們,在運行時,另一個類負責提供這些依賴關系。我們建議在Android APP中使用Google的Dagger 2 來實現依賴注入。Dagger 2通過依賴樹自動創建對象,并在依賴關系上提供編譯時保證。

  • Service Locator:Service Locator提供一個類似注冊表的東西,其中類可以獲取他們的依賴而不是每次都創建他們,與依賴注入(DI)相比,實現起來相對容易,因此如果你不熟悉DI,請使用Service Locator。

這些模式讓你可以很方便的擴展代碼,因為他們提供了清晰的模式來管理依賴關系,而不會重復代碼和增加復雜性。這兩種模式都允許為了測試而替換實現,這也是使用他們的主要好處之一。

在這里例子中,我們將會使用Dagger2來管理依賴關系。

關聯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的實現對于抽象接口調用是很好的,但是因為他僅僅依賴一個數據源,它還不算完美。

上面的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;
    }
}

持久化數據

在目前的實現上,如果用戶旋轉屏幕或者后退再進入這個頁面,因為Repository從內存緩存中獲取數據,所以UI可以瞬間出來。但是如果用戶退出APP過1,2個小時Android系統殺掉APP進程后再進入APP會怎樣呢?

在目前的實現中,我們將需要從網絡中再次獲取數據。這不僅是一個糟糕的用戶體驗,也是浪費,因為它將使用移動數據來重新獲取相同的數據。您可以通過緩存Web請求來簡單地解決這個問題,但它會產生新的問題。如果相同的用戶數據從另一種類型的請求顯示(例如,獲取一個朋友列表)會發生什么情況?那么您的應用程序可能會顯示不一致的數據,這是最令人困惑的用戶體驗。例如,相同的用戶的數據可能會不同,因為朋友列表請求和用戶請求可以在不同的時間執行。您的應用需要合并,以避免顯示不一致的數據。

正確的方法是使用持久化Model來處理,這就是Room持久化庫使用的場景。

Room是一個O/R Mapping庫(類似Hibernate),使用最精簡的代碼提供本地數據持久化。在編譯期,它會按照Schema驗證你的每條查詢,如果發現有問題的SQL語句,它會提示有編譯錯誤,而不是在運行中才暴露問題。Room隱藏了執行原始SQL語句的細節。它還允許觀察數據庫中數據變化(包括集合和連接查詢)。另外,它明確定義線程約束,比如一些常見的問題,在主線程中訪問數據庫。

注意:如果你已經熟悉其他的SQLite ORM或者Realm數據庫持久化方案,你不必替換陳Room,除非你覺得Room更合適。

要使用Room,我們需要定義本地數據庫Schema,首先,用@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);
}

然后在MyDatabase中關聯這個DAO。

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

請注意,load方法一個LiveData<User>。Room知道數據庫何時被修改,當發生數據變化時,他會自動通知所有觀察者。這非常方便,因為只要有一個觀察者它都會更新數據。

注意:從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。這是抽象層帶來的靈活性。這樣對測試也帶來了極大的好處,因為你可以提供一個fake的UserRepository來測試UserProfileViewModel。

現在我們完成了代碼,如果用戶幾天后再回到同一個界面,他們會立即看到用戶信息,因為我們持久化了用戶信息。同時,如果數據過期,Repository會在后臺更新數據。當然,如果持久化的數據太舊了,你可能不希望顯示出來。

在某些情況下,例如下拉刷新時,在UI上顯示告訴用戶當前正在進行網絡請求操作是非常重要的。將UI操作和實際數據分開是很好的做法,因為可能由于各種原因更新(例如,我們獲取朋友列表,則可能獲取相同的用戶觸發LiveData<User>更新)。從UI的角度看,事實上只是同時有另外一個數據請求,就像其他的數據(這里沒翻譯好)。

這里有兩個解決方案:

  • 修改getUser方法以返回一個包含網絡返回狀態的LiveData對象,附錄中提供了一個示例實現:Addendum: exposing network status
  • 在Repository中提供一個可以刷新用戶數據的公共函數。如果你想在UI上顯示網絡狀態,則響應明確的用戶操作(例如下拉刷新)

單一的數據來源(Single source of truth)

不同的REST API通常返回相同的數據。 例如,如果我們的后端有另一個端點返回一個朋友列表,那么同一用戶對象可能來自兩個不同的REST API,也可能是不同的維度。 如果UserRepository按原樣從Webservice請求中返回響應,則我們的UI可能會顯示不一致的數據,因為這些請求之間的服務器端的數據可能會更改。 這就是為什么在UserRepository實現中,Web服務回調只是將數據保存到數據庫中。 然后,對數據庫的更改將觸發活動的LiveData對象上的回調。

在這個模型中,數據庫是單一來源,應用程序的其他部分通過Repository訪問它。 不管您是否使用磁盤緩存,我們建議您的Repository將數據源指定為應用程序其余部分的唯一來源。

測試

我們說過分成隔離的好處可測試性,我們來看看怎樣測試每個模塊的代碼。

  • UI界面和交互:這里是唯一一個需要Android UI Instrumentation test的地方,測試UI代碼最好的方式是創建一個Espresso測試,你可以創建一個fragment,然后提供一個Mock的ViewModel,因為fragment只關聯ViewModel,mock ViewModel可以很方便的完全測試UI。
  • ViewModel:ViewModel可以使用JUnit test來測試,你只需要mock UserRepository對象。
  • UserRepository:你可以使用JUnit來測試,當然你需要mock WebService和DAO。您可以測試它是否進行正確的Web服務調用,將結果保存到數據庫中,如果數據被緩存并且是最新的,則不會發生任何不必要的請求。 既然Webservice和UserDao都是接口,那么你可以mock它們,或為更復雜的測試用例創建假的實現。
  • UserDao:推薦測試DAO的方法是使用 instrumentation tests,因為 instrumentation tests不需要任務UI,而且還算比較快。每個測試,你可以創建一個內存數據庫,確保他們不會有任何副作用(比如,修改磁盤上的數據庫)
    Room也允許指定數據庫實現,所以你可以提供一個SupportSQLiteOpenHelper的是實現來測試,通常不推薦使用此方法,因為在設備上運行的SQLite版本可能與主機上的SQLite版本不同。
  • Testing Artifacts:架構組件提供一個maven artifact來控制后臺線程,android.arch.core:core-testing artifact有兩個JUnit規則:
    • InstantTaskExecutorRule:這個規則用來強制架構組件在當前調用線程馬上執行后臺操作。
    • CountingTaskExecutorRule:這個規則可以用于 instrumentation tests等待架構組件的后臺操作,或者作為空閑資源連接到Espresso。

最終的架構

下圖展示了我們推薦的架構中的所有模塊,以及他們之前如何相互交互的:


final-architecture.png

指導原則

編程屬于創意的領域,構建Android APP也不例外,通常有很多方式來解決問題,無論是在多個Fragment或者Activity之前傳遞數據,獲取遠程數據并存儲在本地用戶離線模式,還是其他常見的一些問題。

雖然一下的建議不是強制的,當時按照我們的經驗,遵循這些建議將使你的代碼更健壯,更加可測試的和可維護性。

  • 在你的manifest文件中定義的入口點 - activities、services、broadcast receivers等,不是你的數據源,相反,他們應該只是協調該入口的相關數據子集。由于每個應用程序組件生命周期非常短暫,取決于用戶與設備的交互以及當時設備的整體運行狀態,所以你不希望這些入口點成為數據源。

  • 在您的應用程序的各個模塊之間創建明確的責任界限。 例如,不要將代碼中加載數據和訪問網絡放在多個類或包中。 同樣,不要將與數據緩存和數據綁定無關的責任放在同一個類中。

  • 每個模塊應該盡可能少的暴露方法,不要試圖為了快速實現從而暴露內部的是實現細節。你可能短時間內會贏得一些時間,但是隨著代碼量的增加,你將會多次付出技術債務。

  • 當你定義模塊之間交互時,請思考如何使每個模塊相互隔離。例如,從網絡獲取數據時定義良好的API將使得更容易測試。相反,如果你把這兩個模塊的邏輯混在一起,或者把從網絡獲取數據的代碼散落在你整個代碼庫的各個地方,那么測試就會非常難以實現。

  • 不要花時間重復造輪子或者一次又一次的編寫相同的代碼。相反,將精力放在如何使你的APP獨一無二的同時,讓Android架構組件和其他推薦的類庫處理重復的勞動。

  • 盡量考慮持久化盡可能多的相關的不過時的數據,這樣你的APP可以處于脫機時使用。要時刻想著,你的用戶可能沒有像你一樣穩定、高速的網絡。

  • Repository應該指定一個單一的數據源,當你的應用程序需要訪問這些數據時,他應該始終源于真實的單一來源。(這里的意思是:獲取網絡的數據,但是只保存到數據庫,然后再通過數據庫的LiveData來通知UI)

附錄:暴露網絡狀態

在上面的“推薦的APP架構”章節,我們故意忽略網絡錯誤和加載狀態以便我們閱讀。在本節中,我們演示一種使用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的流程圖:

network-bound-resource.png

它通過觀察資源的數據庫開始。 當第一次從數據庫加載條目時,NetworkBoundResource將檢查結果是否足夠好以便發送或者應從網絡中獲取。 請注意,這兩個可能同時發生,因為您可能希望在從網絡更新緩存數據時顯示緩存的數據。

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

注意:在將數據保存到磁盤后,我們從數據庫重新初始化流程,但通常我們不這樣做,因為數據庫將會分發更改。另一方面,依賴數據來分發更改也不太好,因為如果數據庫沒有改變,數據庫可以避免分發更改。我們也不想分發更改當從網絡獲取到數據,因為這將違背我們單一來源的原則。我們也不想在沒有新數據的情況下發送SUCCESS,因為它會向客戶端發送錯誤的信息。(這段翻譯不是很好,請直接看原文)

下面是NetworkBoundResource類的公開方法:

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

現在,我們可以在Repository中使用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();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容