Android App Architecture使用詳解

Android應用架構簡介java

 

對於通過過構建app的Android開發人員來講, 如今是時候瞭解一下構建魯棒, 質量高的應用的最佳實踐和推薦架構了.react

這篇文章假設讀者對Android framework比較熟悉.android

OK, let's begin!web

 

App開發人員面臨的常見問題數據庫

 

傳統的桌面開發, 在大多數狀況下, 擁有一個來自Launcher快捷鍵的單獨入口點, 並在獨立的總體進程中運行. 而Android應用則擁有更多複雜的結構. 典型的Android應用由多個應用構件組成, 包括Activities, Fragments, Services, ContentProviders和BroadcastReceivers.編程

大多數這些應用構件聲明在AndroidManifest文件中, 該文件被Android系統使用以將應用整合進全面的用戶體驗中. 儘管, 如先前所言, 傳統的桌面應用做爲一個完整的進程運行, 而正確書寫的Android應用須要更多的靈活性, 由於用戶經過頻繁地切換流和任務, 在設備上不一樣的應用間編寫不一樣的路徑.api

好比, 當你在最喜歡的社交網絡應用上分享照片時, 想一想會發生什麼吧. 應用觸發了一個相機Intent, Android系統經過這個Intent打開相機應用來處理這個請求. 此時, 用戶離開了社交網絡應用, 可是體驗是無縫聯接的. 以後, 相機應用可能觸發其它的Intent, 好比打開文件選擇器, 而文件選擇器可能打開了其它應用. 最後用戶回到了社交網絡應用並分享照片. 並且, 在這些進程, 用戶可能隨時被電話打斷, 以後在打完電話後返回分享照片.緩存

在Android中, 應用跳躍行爲非常廣泛, 因此你的應用必須正確地處理這些流程. 謹記: 移動設備是資源限制的, 由此在任意時刻, 操做系統須要殺掉一些應用, 爲新的應用騰出空間.服務器

關鍵點是: 應用構件可以被獨立且無序地打開, 並且在任意時刻都可以被用戶或者系統銷燬. 由於應用構件是瞬息的, 它們的生命週期(控件被建立和銷燬的時刻)並不受你控制, 由此, 在應用構件中不該該存儲任何應用數據和狀態, 並且應用構件之間不該該依賴於彼此.網絡

 

通用架構規則

 

若是你不能使用應用構件保存應用數據和狀態, 應用應該怎麼組織?

你應該關注的最重要的事件是:關注分離. 常見的一個錯誤時: 在單個Activity/Fragment中寫所有的代碼. 任何不處理UI和操做系統交互的代碼不該該寫在這些類裏面. 儘量保持簡潔會容許你避免生命週期相關的問題. 不要忘記: 你不擁有這些類, 它們僅僅是膠水類, 象徵了操做系統和應用之間的協議. Android操做系統可能隨時基於用戶交互或者其它諸如內存不足等因素銷燬它們. 要提供穩定的用戶體驗, 最好最小化對它們的依賴.

第二個重要的規則是: 使用模型, 尤爲是持久化模型驅動UI. 持久化因兩個緣由而完美: 若是操做系統銷燬了應用以釋放資源, 用戶不會丟失數據; 在網絡鏈接弱或者未鏈接時, 應用可以持續工做. 而模型就是爲用戶處理數據的構件. 它們獨立於應用中的視圖和應用構建, 由此它們絕緣於這些構件的生命週期問題. 保持UI代碼簡單且獨立於應用邏輯使得管理更加簡便. 經過定義良好的數據管理職責, 將應用基於模型類, 將使得模型類更加容易測試, 使得應用更具一致性.

 

推薦的應用架構

 

下面將經過用例使用架構組件來組織應用.
備註: 不可能有一種寫應用的方式對全部場景都是最好的. 也就是說, 這裏推薦的架構對於大多數用例應該是好的起點. 若是你已經有了好找寫Android應用的方式, 你無需改變.

想像一下, 構建一個展現用戶概況的UI. 用戶概況將會使用REST API從自有私有後臺拉取.

 

構建user interface

 

UI由UserProfileFragment.java和對應的user_profile_layout.xml組成.

要驅動UI, 咱們的數據模型須要持有兩個數據元素.

  • User ID: 用戶標識符. 最好使用fragment的arguments將這些信息傳給fragment. 若是Android系統銷燬了你的進程, 這個信息將會被保存, 因此在下次應用重啓時, id是可用的.
  • User object: 持有用戶數據的POJO.

咱們會基於ViewModel類建立UserProfileViewModel來保持信息.
ViewModel爲特定的UI構件, 諸如Activity/Fragment, 提供數據, 並處理與數據處理的業務部分的通信, 諸如調用其它構件加載數據或者提交用戶修改. ViewModel並不知曉視圖, 且並不受諸如因爲屏幕旋轉致使的Activity重建等配置改變的影響.

如今咱們有三個文件:

  • user_profile.xml: 爲屏幕定義的UI.
  • UserProfileViewModel.java: 爲UI準備數據的類.
  • UserProfileFragment.java: 在ViewModel中展現數據和對用戶交互反饋的UI控制器.

下面是起始實現(爲簡單起見, layout文件沒有提供):

 1 public class UserProfileViewModel extends ViewModel {
 2     private String userId;
 3     private User user;
 4 
 5     public void init(String userId) {
 6         this.userId = userId;
 7     }
 8     public User getUser() {
 9         return user;
10     }
11 }
 1 public class UserProfileFragment extends Fragment {
 2     private static final String UID_KEY = "uid";
 3     private UserProfileViewModel viewModel;
 4 
 5     @Override
 6     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
 7         super.onActivityCreated(savedInstanceState);
 8         String userId = getArguments().getString(UID_KEY);
 9         viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
10         viewModel.init(userId);
11     }
12 
13     @Override
14     public View onCreateView(LayoutInflater inflater,
15                 @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
16         return inflater.inflate(R.layout.user_profile, container, false);
17     }
18 }


如今, 咱們有三個代碼模塊, 咱們該如何鏈接? 畢竟, ViewModel的user域設置的時候, 咱們須要一種方式通知UI. 這就是LiveData出現的地方:

LiveData是可觀測的數據持有者. 它讓應用組件觀測LiveData對象的改變, 卻沒有建立顯著且嚴格的依賴路徑. LiveData也尊重應用組件的生命週期狀態, 作正確的事防止對象泄露, 保證應用並沒消費更多的內存.

備註: 若是你在使用諸如RxJava/Agera庫, 你能夠繼續使用它們而沒必要使用LiveData. 可是當你使用它們和其它的途徑的時候, 確保你在正確地處理生命週期, 確保在相關的LifecycleOwner中止的時候, 數據流暫停; 在LifecycleOwner銷燬的時候, 數據流被銷燬. 你也能夠添加 android.arch.lifecycle:reactivestreams 和其它的reactive庫(好比, RxJava2)一塊兒使用LiveData.

如今咱們使用LiveData<User>取代UserProfileViewModel裏的User域, 確保在數據更新的時候, fragment可以被通知到. LiveData的一個好處是: 它是可感知生命週期的, 在引用再也不須要的時候, LiveData會自動地清除它們.

1 public class UserProfileViewModel extends ViewModel {
2     ...
3     private User user; 4     private LiveData<User> user; 5     public LiveData<User> getUser() {
6         return user;
7     }
8 }

如今修改UserProfileFragment, 來觀測數據並更新UI.

1 @Override
2 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
3     super.onActivityCreated(savedInstanceState);
4     viewModel.getUser().observe(this, user -> {
5       // update UI
6     });
7 }

每一次用戶數據更新, onChanged()回調都會被觸發, UI會被刷新.

若是你熟悉於其它的使用可觀測回調的庫, 你也許已經發覺咱們沒必要覆蓋fragment的onStop()方式來中止觀測數據. LiveData也是沒必要要的, 由於它是可以感知生命週期的, 這意味着回調不會被調用, 除非fragment處於活躍狀態(接收onStart()方法卻沒有接收到onStop()方法). 在fragment接收到onDestroy()時, LiveData也會自動地刪除觀測者.

咱們沒必要作任何特殊的事情來處理配置改變(好比, 使用旋轉屏). 在配置改變或者fragment恢復生機的時候, ViewModel會自動地保存. 它會接收到相同的ViewModel實例, 回調會自動地使用當前數據回調. 這就是爲何不該該直接引用視圖; 它們比視圖的生命週期活得更久.

 

拉取數據

 

如今已經將ViewModel鏈接到Fragment, 可是ViewModel若是拉取用戶數據呢? 在這個例子中, 假設後臺提供的是REST API. 咱們會使用Retrofit庫來訪問後臺, 儘管你可使用不一樣的庫來實現相同的目標.

這是咱們的Retrofit Webservice, 來和後臺通訊:

1 public interface Webservice {
2     /**
3      * @GET declares an HTTP GET request
4      * @Path("user") annotation on the userId parameter marks it as a
5      * replacement for the {user} placeholder in the @GET path
6      */
7     @GET("/users/{user}")
8     Call<User> getUser(@Path("user") String userId);
9 }

實現ViewModel的每個想法也是是直接調用Webservice, 來拉取數據, 並把它賦值給用戶對象. 儘管這樣能夠工做, 可是當隨着應用不斷增加, 維護將變得很困難. 這樣會給予ViewModel太多的責任, 而ViewModel是違反先前提到的"關注分離"原則. 除此以外, ViewModel的範圍綁定到了Activity/Fragment, 因此在生命週期結束的時候失去數據是個不好的用戶體驗. 相反, ViewModel會代碼這個工做到一個新Repository模塊.

Repository模型負責處理數據操做. 它們給應用提供了乾淨的API. 它們知道從哪獲取數據, 知道在數據更新時, 調用什麼API. 你能夠把它們看成不一樣數據源(持久化模型, 網頁服務, 緩存等)之間的中介.

以下UserRepository類使用了Webservice拉取用戶數據項.

 1 public class UserRepository {
 2     private Webservice webservice;
 3     // ...
 4     public LiveData<User> getUser(int userId) {
 5         // This is not an optimal implementation, we'll fix it below
 6         final MutableLiveData<User> data = new MutableLiveData<>();
 7         webservice.getUser(userId).enqueue(new Callback<User>() {
 8             @Override
 9             public void onResponse(Call<User> call, Response<User> response) {
10                 // error case is left out for brevity
11                 data.setValue(response.body());
12             }
13         });
14         return data;
15     }
16 }

儘管Repository模塊看起來很沒必要要, 但它服務於一個重要目的; 它抽象了數據源. 如今, 咱們的ViewModel並不知道被Webservice拉取的數據, 這意味着咱們有必要將它轉化成別的實現.


管理組件之間的依賴

 

以上的UserRepository類須要Webservice實例來作它的工做. 能夠簡單地建立它, 但若是要這麼作的話, 須要知道Webservice類以構建該類. 這樣作會顯著地使代碼更加複雜和重複(每個須要Webservice實例的類須要知道如何使用它的依賴來構建類). 除此以外, UserRepository極可能不是惟一須要Webservice的類. 若是每個類都建立了新的Webservice, 這將很是消耗資源.

你可使用兩種方式解決這個問題:

  • Dependency Injection: 依賴注入容許類在不構建實例的前提下定義他們的依賴. 在運行時, 其它的類負責提供這些依賴. 推薦使用Google的Dagger 2在Android應用中實現依賴注入. Dagger 2經過遍歷依賴樹自動地構建對象, 並提供對依賴的編譯期保證.
  • Service Locator: 服務定位器提供一個註冊, 使得類可以獲取它們的依賴而非構建它們. 實現依賴注入(DI)相對容易, 因此若是你不熟悉DI, 那就使用Service Locator吧.

這些模式容許你衡量本身的代碼, 由於他們爲在沒有重複代碼和添加複雜性的的前提下管理依賴提供了清晰的模式. 二者都容許將實現轉換成測試; 這是使用二者的主要好處.

 

鏈接ViewModel和Repository

 

下面代碼展現瞭如何修改UserProfileViewModel以使用repository.

 1 public class UserProfileViewModel extends ViewModel {
 2     private LiveData<User> user;
 3     private UserRepository userRepo;
 4 
 5     @Inject // UserRepository parameter is provided by Dagger 2
 6     public UserProfileViewModel(UserRepository userRepo) {
 7         this.userRepo = userRepo;
 8     }
 9 
10     public void init(String userId) {
11         if (this.user != null) {
12             // ViewModel is created per Fragment so
13             // we know the userId won't change
14             return;
15         }
16         user = userRepo.getUser(userId);
17     }
18 
19     public LiveData<User> getUser() {
20         return this.user;
21     }
22 }

緩存數據

以上的repository實現對於抽象對於網絡服務的調用是有好處的, 由於它只依賴了惟一一個數據源, 但這並不十分有不少用處.

使用以上UserRepository實現的問題是, 在拉取了數據以後, 並不保存數據. 若是用戶離開了UserProfileFragment而後再回來, 應用不會從新拉取數據. 這是很差的, 由於: 它既浪費了寶貴的網絡帶寬, 又強制用戶等待查詢的完成. 要解決這個問題, 咱們將向UserRepository添加新的數據源, 而新數據源將會在內存中緩存User對象.

 1 @Singleton  // informs Dagger that this class should be constructed once
 2 public class UserRepository {
 3     private Webservice webservice;
 4     // simple in memory cache, details omitted for brevity
 5     private UserCache userCache;
 6     public LiveData<User> getUser(String userId) {
 7         LiveData<User> cached = userCache.get(userId);
 8         if (cached != null) {
 9             return cached;
10         }
11 
12         final MutableLiveData<User> data = new MutableLiveData<>();
13         userCache.put(userId, data);
14         // this is still suboptimal but better than before.
15         // a complete implementation must also handle the error cases.
16         webservice.getUser(userId).enqueue(new Callback<User>() {
17             @Override
18             public void onResponse(Call<User> call, Response<User> response) {
19                 data.setValue(response.body());
20             }
21         });
22         return data;
23     }
24 }

持久化數據

在咱們當前的實現中, 若是用戶旋轉了屏幕或者離開後再返回應用, 已存在UI將會當即可見, 由於repository從內存緩存中檢索了數據. 可是若是用戶離開了應用幾個小時以後再返回呢, 在Android系統已經殺死了進程以後?

若是使用當前實現的話, 咱們須要從網絡中再次拉取數據. 但這不只是個壞的用戶體驗, 並且浪費, 由於它使用了移動數據從新拉取相同的數據. 經過緩存網頁請求, 你能夠簡單地修復這個問題. 但這又產生了新的問題. 若是相同的用戶數據從其它類型的網頁請求中展現出來呢? 好比, 拉取好友列表. 你的應用極可能會展現不一致的數據, 這充其量是一個混淆的用戶體驗. 舉個例子, 相同用戶的數據可能展現的不同, 由於好友列表請求和用戶請求可能在不一樣的時刻執行. 應用須要合併兩個請求以免展現不一致的數據.

處理這些的正確方式是使用持久化模型. 這就是爲何要使用Room持久化庫.

Room是一個對象映射庫, 使用最少的樣板代碼提供本地數據持久化. 在編譯時, 根據計劃驗證每個查詢, 由此, 壞的SQL請求展現編譯期錯誤而非運行時失敗. Room抽象了使用原生SQL表和查詢的基本的實現細節. 它也容許觀測數據庫中數據的改變(包括集合和聯接查詢), 經過LiveData對象暴露這些改變. 此外, 它顯式地定義了線程約束: 在主線程中訪問存儲.
備註: 若是應用已經使用了其它的諸如SQLite對象關係型映射的持久化解決方案, 你沒必要用Room取代它們. 然而, 若是你在寫新應用或者重構已有應用, 推薦使用Room持久化應用數據. 經過這種方式, 你可以充分利用庫的抽象和查詢驗證能力.

要使用Room, 我須要定義本地schema. 首先, 用@Entity註解User類, 把它在數據庫中標記爲表:

1 @Entity
2 class User {
3   @PrimaryKey
4   private int id;
5   private String name;
6   private String lastName;
7   // getters and setters for fields
8 }


以後, 經過繼承RoomDatabase建立數據庫:

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


你能夠注意到MyDatabase是抽象類. Room會自動地提供它的實現.
如今咱們須要一種方式在用戶數據插入數據庫. 首先建立Data Access Object)(DAO):

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


以後從數據庫類中引用DAO.

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


請注意load()方法返回LiveData<User>. Room知道數據庫修改的時間, 它會在數據發生改變時自動地通知全部依舊活躍的觀測者. 由於在使用LiveData, 它會很是高效, 由於只有在至少一個活躍觀測者時, 纔會更新數據.
備註: Room基於數據表的修改來檢測認證, 這意味着分發假陽性通知.

如今咱們可以修改UserRepository來跟Room數據源合做:

 1 @Singleton
 2 public class UserRepository {
 3     private final Webservice webservice;
 4     private final UserDao userDao;
 5     private final Executor executor;
 6 
 7     @Inject
 8     public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
 9         this.webservice = webservice;
10         this.userDao = userDao;
11         this.executor = executor;
12     }
13 
14     public LiveData<User> getUser(String userId) {
15         refreshUser(userId);
16         // return a LiveData directly from the database.
17         return userDao.load(userId);
18     }
19 
20     private void refreshUser(final String userId) {
21         executor.execute(() -> {
22             // running in a background thread
23             // check if user was fetched recently
24             boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
25             if (!userExists) {
26                 // refresh the data
27                 Response response = webservice.getUser(userId).execute();
28                 // TODO check for error etc.
29                 // Update the database.The LiveData will automatically refresh so
30                 // we don't need to do anything else here besides updating the database
31                 userDao.save(response.body());
32             }
33         });
34     }
35 }


請注意: 儘管咱們在UserRepository裏面修改了數據來源, 但並不須要修改UserProfileViewModel和UserProfileFragment. 這就是抽象提供的靈活性. 這也對測試友好, 由於在測試UserProfileViewModel的時候, 你可以提供假的UserRepository.

如今代碼完整了. 若是用戶稍後回到相同的UI, 它們依然可以看到用戶信息, 由於信息已經持久化了. 同時, 若是數據髒了的話, 咱們的Repository會在後臺更新它們. 固然, 取決於你的用例, 若是持久化數據太老的話, 你也許選擇不展現.

在一些用例中, 好比下拉刷新, 若是當前有網絡操做正在進行, 在UI上向用戶展現進度也很重要. 將UI操做與真實的數據分離開來是最佳實踐, 由於UI操做會根據不一樣的版本更新(好比, 若是咱們拉取好友列表, 用戶可能經過觸發LiveData<User>更新而再次拉取). 從UI的角度看, 有請求正在的進行是另外一個數據點, 類似於其它的碎片數據(好比User對象).

這個用例有兩個通用解決方案:

  • 修改getUser返回LiveData, 使它包含網絡操做的狀態.
  • 在Repository類中提供另外一個公共方法, 可以刷新用戶狀態. 若是你只想在UI中展現網絡狀態做爲顯式用戶操做(像下拉刷新)的迴應的話, 這個選項更好.

單一真理之源

對於不一樣的REST API, 返回相同的數據, 是很廣泛的. 舉個例子, 若是後臺有另外一個終點返回好友列表, 相同的用戶對象可能來自兩個不一樣的API終點, 也許以不一樣的粒度. 若是UserRepository註定返回來自Webservice的請求的響應, UI可能會展現不一致的數據, 由於在後臺的數據在兩個請求之間能夠發生改變. 這就是爲何在UserRepository實現中, 網頁服務回調將數據存儲進數據庫中. 以後, 數據庫的改變會在LiveData對象上面觸發回調.

在這個模型中, 數據庫做爲真理的單一之源, 應用的其它部分經過Repository訪問它. 不管是否使用硬盤緩存, 推薦: repository指派數據源做爲應用的單一真理之源.

測試

以前已經提到過: 分享的優點之一就是可測試性. 接下來看一下如何測試每個模塊:

  • 用戶接口和交互: 這是惟一的一次須要使用Android UI Instrumentation測試. 測試UI代碼的最好方式是建立Espresso測試. 能夠建立Fragment, 而後提供一個模擬的ViewModel. 由於Fragment只跟ViewModel交流, 模擬ViewModel對於徹底測試UI是很充分的.
  • ViewModel: ViewModel可能經過JUnit來測試. 須要模擬惟一的UserRepository.
  • UserRepository: 也能夠經過JUnit測試UserRepository. 只不過須要模擬Webservice和DAO. UserRepository調用正確的網頁服務, 把結果保存到數據庫, 而且, 若是數據緩存過而且是最新的, 也不會發出沒必要要的請求. 由於Webservice和UserDao都是接口, 你可以模擬它們或者爲更復雜的測試用例建立僞實現.
  • UserDao: 測試DAO類的推薦途徑是使用instrumentation測試. 由於instrumentation測試並不要求UI, 依然運行地很快. 對於每個測試, 能夠建立內存內數據庫以保障測試並不會擁有任何負做用(例如改變硬盤裏面數據庫文件). Room也容許指定數據庫實現, 因此能夠經過給它提供SupportSQLiteOpenHelper的JUnit實現來測試它. 這個方式一般並不值得推薦, 由於不一樣設備上的SQLite版本也許和虛擬機上的SQLite版本不一樣.
  • Webservice: 進行獨立於外部世界的測試很重要. 因此, 即便是Webservice測試也應該避免向後臺進行網絡請求. 有大量的庫能夠作到這些. 舉個例子, MockWebServer是個很好的包, 幫助你建立僞的本地服務器.
  • Testing Artifacts結構組件提供了maven artifact來控件後臺線程. 在android.arch.core:core-testing內部, 有兩個JUnit規則:
  1. InstantTaskExecutorRule: 這個規則使用強制結構組件在調用者線程上即時執行任何後臺操做.
  2. CountingTaskExecutorRule: 這個規則在instrumentation測試中使用, 以等待結構組件的後臺操做, 或者做爲空閒資源鏈接給Espresso.

最終架構

下面的圖表展現了推薦架構中全部的模塊, 以及它們之間如何交互:

指導原則

編程是具備創造性的領域, 構建Anroid應用也不例外. 有不少種方式來解決一個問題. 這個問題能夠是在多個activities/fragments之間通訊, 檢索遠程數據並將它做爲離線模式在本地持久化, 或者其它多數應用遇到的常見場景.

儘管下面的推薦並不是強制, 但長久來看, 遵循它們會使得代碼更多魯棒, 可測試和可維護.

  • 在AndroidManifest中定義的入口--activities, services, broadcast receivers--並非數據源. 相反, 它們應該只是協同於入口相關數據的子集. 由於每個應用組件都活得至關短, 依賴於用戶與設備的交互和運行時當前的徹底健康情況, 你不會想任何入口成爲數據源的.
  • 在應用的不一樣模塊間毫無憐憫地建立定義良好的責任邊界. 好比, 不要把網絡加載數據的代碼分散到不一樣的類和包. 並且也不要將不相關的責任填在相同的類裏面, 例如數據緩存和數據綁定.
  • 模塊間儘量少地暴露細節. 不要嘗試去建立暴露其它模塊實現細節的快捷入口. 短時間看可能節省一些時間, 可是隨着代碼的進化, 你會多付好幾倍的技術債.
  • 在定義模塊間交互的時候, 考慮如何單獨測試每一個模塊. 好比, 擁有定義良好的網絡拉取數據的API會使得測試在本地數據庫持久化數據的模塊更易測試. 若是你在一個地方混合了兩個模塊的邏輯, 或者將網絡相關代碼弄得整個項目都是, 測試將會很難.
  • 應用的核心是應用的突出點. 不要花時間重複造輪子或者一遍又一遍地寫模板代碼. 集中精神力量於應用獨一無二的地方, 讓Android結構組件和其它的推薦庫去處理模板代碼.
  • 儘量將相關性大和新鮮的數據持久化, 這樣你在應用在離線模式下也是可用的. 你可能享受穩定的高速的鏈接, 用戶可未必.
  • Repository應用指定單一真理數據源. 不管任什麼時候候應用須要訪問數據, 都應該從單一真理數據源產生.

附錄: 暴露網絡狀態

使用Resource類封閉數據和狀態.
下面是實現示例:

 1 //a generic class that describes a data with a status
 2 public class Resource<T> {
 3     @NonNull public final Status status;
 4     @Nullable public final T data;
 5     @Nullable public final String message;
 6     private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
 7         this.status = status;
 8         this.data = data;
 9         this.message = message;
10     }
11 
12     public static <T> Resource<T> success(@NonNull T data) {
13         return new Resource<>(SUCCESS, data, null);
14     }
15 
16     public static <T> Resource<T> error(String msg, @Nullable T data) {
17         return new Resource<>(ERROR, data, msg);
18     }
19 
20     public static <T> Resource<T> loading(@Nullable T data) {
21         return new Resource<>(LOADING, data, null);
22     }
23 }


由於邊從網絡加載數據邊從硬盤展現是常見的用例, 建立一個能在不一樣場合重用的幫助類NetworkBoundResource. 下面是NetworkBoundResource的決定樹:

它從觀測數據庫資源開始. 在入口首次從數據庫加載的時候, NetworkBoundResource檢測結果是否足夠好來分發或者它應該從網絡拉取. 這兩個場景有可能同時發生, 由於你能夠一邊展現緩存數據, 一邊從網絡更新該數據.

若是網絡調用成功完成, 它將響應結果保存內數據庫並從新初始化流. 若是網絡請求失敗, 直接分發錯誤結果.
備註: 在保存新的數據到硬盤以後, 咱們從數據庫中從新初始化, 儘管, 一般咱們並不須要這樣作, 由於數據庫會分發這些變化. 另外一方面, 依賴數據庫頒發這些改變依賴於很差的負做用, 由於若是數據沒有發生改變, 數據庫可以避免分發這些改變. 咱們也不想分發產生自網絡的結果, 由於這違反了單一真理之源原則(也許有數據庫裏有觸發器, 會改變保存的值). 咱們也不想要在沒有新數據的狀況下分發SUCCESS, 由於它會向客戶端發送錯誤的信息.

如下是NetworkBoundResource爲子類提供的公共API:

 1 // ResultType: Type for the Resource data
 2 // RequestType: Type for the API response
 3 public abstract class NetworkBoundResource<ResultType, RequestType> {
 4     // Called to save the result of the API response into the database
 5     @WorkerThread
 6     protected abstract void saveCallResult(@NonNull RequestType item);
 7 
 8     // Called with the data in the database to decide whether it should be
 9     // fetched from the network.
10     @MainThread
11     protected abstract boolean shouldFetch(@Nullable ResultType data);
12 
13     // Called to get the cached data from the database
14     @NonNull @MainThread
15     protected abstract LiveData<ResultType> loadFromDb();
16 
17     // Called to create the API call.
18     @NonNull @MainThread
19     protected abstract LiveData<ApiResponse<RequestType>> createCall();
20 
21     // Called when the fetch fails. The child class may want to reset components
22     // like rate limiter.
23     @MainThread
24     protected void onFetchFailed() {
25     }
26 
27     // returns a LiveData that represents the resource, implemented
28     // in the base class.
29     public final LiveData<Resource<ResultType>> getAsLiveData();
30 }


注意, 以上類定義了兩個類型參數(ResultType, RequestType), 由於API返回的數據類型也許並不匹配本地使用的數據類型.
也要注意, 以上代碼使用了ApiResponse用於網絡請求. ApiResponse是個簡單的Retrofit2.Call包裹類, 將響應轉變成LiveData.

如下是NetworkBondResource實現的餘下部分:

 1 public abstract class NetworkBoundResource<ResultType, RequestType> {
 2     private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
 3 
 4     @MainThread
 5     NetworkBoundResource() {
 6         result.setValue(Resource.loading(null));
 7         LiveData<ResultType> dbSource = loadFromDb();
 8         result.addSource(dbSource, data -> {
 9             result.removeSource(dbSource);
10             if (shouldFetch(data)) {
11                 fetchFromNetwork(dbSource);
12             } else {
13                 result.addSource(dbSource,
14                         newData -> result.setValue(Resource.success(newData)));
15             }
16         });
17     }
18 
19     private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
20         LiveData<ApiResponse<RequestType>> apiResponse = createCall();
21         // we re-attach dbSource as a new source,
22         // it will dispatch its latest value quickly
23         result.addSource(dbSource,
24                 newData -> result.setValue(Resource.loading(newData)));
25         result.addSource(apiResponse, response -> {
26             result.removeSource(apiResponse);
27             result.removeSource(dbSource);
28             //noinspection ConstantConditions
29             if (response.isSuccessful()) {
30                 saveResultAndReInit(response);
31             } else {
32                 onFetchFailed();
33                 result.addSource(dbSource,
34                         newData -> result.setValue(
35                                 Resource.error(response.errorMessage, newData)));
36             }
37         });
38     }
39 
40     @MainThread
41     private void saveResultAndReInit(ApiResponse<RequestType> response) {
42         new AsyncTask<Void, Void, Void>() {
43 
44             @Override
45             protected Void doInBackground(Void... voids) {
46                 saveCallResult(response.body);
47                 return null;
48             }
49 
50             @Override
51             protected void onPostExecute(Void aVoid) {
52                 // we specially request a new live data,
53                 // otherwise we will get immediately last cached value,
54                 // which may not be updated with latest results received from network.
55                 result.addSource(loadFromDb(),
56                         newData -> result.setValue(Resource.success(newData)));
57             }
58         }.execute();
59     }
60 
61     public final LiveData<Resource<ResultType>> getAsLiveData() {
62         return result;
63     }
64 }


如今, 咱們可以經過在repository中綁定User實現來使用NetworkBoundResource寫硬盤和網絡.

 1 class UserRepository {
 2     Webservice webservice;
 3     UserDao userDao;
 4 
 5     public LiveData<Resource<User>> loadUser(final String userId) {
 6         return new NetworkBoundResource<User,User>() {
 7             @Override
 8             protected void saveCallResult(@NonNull User item) {
 9                 userDao.insert(item);
10             }
11 
12             @Override
13             protected boolean shouldFetch(@Nullable User data) {
14                 return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
15             }
16 
17             @NonNull @Override
18             protected LiveData<User> loadFromDb() {
19                 return userDao.load(userId);
20             }
21 
22             @NonNull @Override
23             protected LiveData<ApiResponse<User>> createCall() {
24                 return webservice.getUser(userId);
25             }
26         }.getAsLiveData();
27     }
28 }
相關文章
相關標籤/搜索