注意
Guide to App Architectureの和訳になります。
わかりづらい表現を意訳したり、回りくどいところを端折ったりしています。
和訳に自信がないところもあるため、間違いを見つけた場合は指摘してください。
開発者が直面する一般的な問題
デスクトップのショートカットからのみ起動さされる従来のデスクトップアプリケーションとは異なり、Androidアプリははるかに複雑な構造になっています。
一般的なAndroidアプリは Activity、Fragment、Services、コンテンツプロバイダー、ブロードキャストレシーバーなど、複数のコンポーネントから構成されています。
これらのコンポーネントはAndroidMainifestに宣言されいます。
AndroidManifestはAndroid OSで使用され、デバイス上でのアプリの振る舞いが定義されています。
従来のデスクトップアプリケーションでは単独で動作していましたが、Androidアプリではデバイスにインストールされている様々なアプリを呼び出したり、フローとタスクの常に切り替えるため、柔軟に対応する必要があります。
例えば、SNSアプリで写真を共有する時のことを考えてみましょう。
アプリがカメラアプリを起動するためのインテントを発行し、Android OSがカメラアプリを起動します。
この時点でユーザはSNSアプリから離れますが、シームレスで行われます。
カメラアプリはフォルダから画像を選択させるために別のインテントを発行するかもしれません。
最終的にユーザはSNSアプリに戻って写真を共有します。
また、このプロセスの間に電話がかかってきた場合でも電話終了後に写真を共有することができます。
上記のポイントとして、あなたのアプリのコンポーネントは個別に起動することができ、順不同で起動され、ユーザやAndroid OSによっていつでも破棄できると言う事です。
開発者がライフサイクルをコントロールすることはできず、ユーザやシステムによっていつ破棄・生成が行われるかわかりません。
そのため、表示データやUIの状態をActivty、Fragmentで保持するべきではありません。
アーキテクチャの原則
アプリコンポーネントでアプリの状態やでデータを保存できない場合、どうすれば良いでしょうか?
各機能を分離することがとても重要です。
ActivityやFragmentに機能を持たせすぎることはよくある間違いです。
UIやOSに対する機能以外をActivity、Fragmentに持たせるべきではありません。
ライフサイクルに関する多くの問題を避けるためには、Activity、Fragmentの依存関係を最低限に抑えることが最善です。
もう一つの大事なことは、Modelできれば、永続的なModelからUIの操作を行うことです。
データ永続化することによって、通信が不安定だったりオフラインの場合でもアプリを動作し続けることができ、ユーザのデータが失われなくなります。
Modelはアプリのデータを処理するコンポーネントです。
ModelをViewとコンポーネント(Activity、Fragmentなど)から独立させることによって、Modelはライフサイクルの問題から解放されますし、コードがシンプルになり管理が容易になります。
また、データを管理するModelクラスを明確に分けることでテストが可能になります。
推奨されるアプリのアーキテクチャ
このセクションではユースケースを通してアーキテクチャコンポーネントについて説明します。
ユーザーインターフェイスの構築
ユーザのプロフィールを表示する画面を想像してください。
ユーザプロフィールはREST APIから取得します。
画面構成はUserProfileFragment.java
とuser_profile_layout.xml
からなります。
データModelは2つのフィールド変数を持ちます。
・ユーザID:ユーザを特定するIDです。このIDはアプリがOSによって破棄される際に保存されるので、アプリを再表示される際にも使用できます。
・ユーザオブジェクト:ユーザデータのPOJOです。
これらのデータを保持するためにUserProfileViewModel
を作成します。
ViewModel
ViewModelはActivityやFragmentのUIに表示するデータや状態を提供し、データの読み込みやユーザデータを更新するためにAPI通信ロジックの呼び出しを行います。
ViewModelはView(Activity、Fragmentなど)から独立しているので、画面回転などで画面が再生成されてもデータが破棄されることはありません。
以上3つのファイルを作成します。
・ user_profile.xml : 画面のレイアウト
・ UserProfileViewModel.java : UIに表示するデータや状態を保持するクラス
・ UserProfileFragment.java : ViewModelのデータを表示したり、ユーザアクションに反応するUIコントローラ
下記が実装内容になります。(レイアウトは単純なので省略)
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);
}
}
注:Fragmentの代わりにLifecycleFragmentを継承しています。LifecycleOwnerを実装したFragmentがAndroidサポートライブラリに同梱される予定です。
3つのモジュールが揃ったところで、これらをどうやって繋げましょう?
ViewModelのuser
にデータがセットされた時にUIに変更を伝える必要があります。
これにはLiveDataを使用します。
LiveData
LiveDataはObservableなデータホルダーです。
各コンポーネントは値が変わったことを伝えるリスナーなどを実装しなくても、値の変更を知ることができます。
また、LiveDataはコンポーネント(Activity、Fragmentなど)のライフサイクル状態を考慮し、メモリリークが発生しないように設計されています。
注:すでにRxJavaやAgeraを使用している場合は、そのまま使い続けても問題ありません。
しかし、RxJavaやAgeraを使う場合には、ライフサイクルを考慮してStreamの破棄を適切に処理する必要があります。
また、android.arch.lifecycle:reactivestreams
を追加して、別のリアクティブストリームライブラリ(RxJava2など)でLiveDataを使用することもできます。
それではUserProfileViewModel
のUser
をLiveData<User>
に置き換えてみます。
これによって、FragmentはUser
データの変更を知ることができます。
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が更新されます。
RxJavaなどを使用している方はonStop()
をオーバーライドしてunsubscribe()
していないことに気づいたかもしれません。
LiveDataはライフサイクルを考慮しているため、Fragmentがアクティブな状態でない場合はコールバックを呼び出しません。FragmentがonDestory()
を受け取ると、LiveDataは自動的にオブザーバも破棄します。
また、ViewModel
を使用する事で、画面の再生成時(画面回転などによって)にonSaveInstanceState()
でデータを保存する必要もなくなります。
新しく生成されたFragmentは再生成前と同じViewModel
のインスタンスを受け取ることができるので、再生成前に使用していたデータをすぐに使うことができます。
ViewModel
はコンポーネント(Activity、Fragmentなど)のライフサイクルよりも長く生存するため、Viewを直接参照しないようにしてください。詳しくはThe lifecycle of a ViewModelを参照してください。
データの取得
ViewModelとFragmentをつなげたところで、次はViewModelでデータの取得を行います。
この例ではREST APIからユーザデータを取得します。API通信には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で直接Webサービスを呼び出すこともできますが、将来的に機能が増えるにつれてメンテナンスが難しくなります。
推奨されるアプリのアーキテクチャで述べたように、機能ごとに分離することが重要です。
ViewModelはActivity、Fragmentのライフサイクルに関連づけられているため、ライフサイクルが終了すると(完全に破棄されると)データも失うことになります。
これではユーザエクスペリエンスが悪くなります。
データが失われないように、この作業をRepository
モジュールに委譲します。
Repository
Repositoryはデータ操作を処理します。
Repositoryは取得、更新に応じて異なるデータソース(DB、Webサービス、キャッシュなど)からデータにアクセスします。
データソースのアクセスをRepositoryに任せることで、呼び出し元のViewModelではどこからデータを取得するのかを気にする必要がなくなります。
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;
}
}
注:わかりやすくするために、この例ではネットワークエラーの場合を考慮していません。
エラー対応の実装についてはAppendix:ネットワーク状態を公開するを参照してください。
コンポーネント間の依存関係の管理
UserRepository
クラスではWebservice
のインスタンスが必要になります。
インスタンスの生成には依存関係を知っている必要があります。
他のRepositoryクラスでもWebservice
のインスタンスが必要になるかもしれません。
Webサービスを使用するRepositoryクラスごとにWebservice
のインスタンスを生成していると冗長的ですし、リソースを圧迫することになります。
この問題を解決する2つのパターンがあります。
依存性の注入
コンポーネント間の依存関係をプログラムのソースコードから排除し、外部の設定ファイルなどで注入できるようにするソフトウェアパターンである。(Wikipediaより)
Androidアプリで依存性の注入をするにはGoogle製のDagger 2をおすすめします。
Dagger 2は自動的に依存関係のツリーを構築して、コンパイル時に依存性の注入を行います。
サービスロケータパターン
サービスロケータパターンは、クラスが依存関係を構築するのではなく、依存関係を取得できるレジストリを提供します。
依存性の注入(DI)よりも実装が比較的容易なため、DIに慣れていない場合は代わりにサービスロケータパターンを使用してください。
これらのパターンを使用する大きなメリットの1つとして、Webサービスから取得していたデータをテストの時だけローカルDBから取得するようにしたい場合でも簡単に切り替えができるようになります。
今回の例ではDagger 2を使用して依存関係を管理します。
ViewModelとリポジトリの接続
リポジトリを使用するためにUserProfileViewModel
を修正します。
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // 引数のUserRepositoryはDagger 2によって依存の注入がされます
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// 各FragmentでViewModelを生成しますが、UserIDは同じです
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}
データのキャッシュ
上記のUserRepository
の実装は、Webサービスへの呼び出ししか行なっていいないため、あまり機能的ではありません。
UserRepository
の実装の問題点としてはデータを取得した後にキャッシュしていないことです。
UserProfileFragment
が破棄されて、再度ユーザがUserProfileFragment
を表示した場合はまたデータを取得し直す必要があります。
これでは無駄なネットワーク通信を行い、通信が終わるまでユーザを待たせてしまいます。
この対応として、UserRepository
にUser
データをメモリにキャッシュするためのデータソースを追加します。
@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;
}
}
データの永続化
現在の実装ではユーザが画面を回転して画面が再生成されても、リポジトリがメモリ内のキャッシュからデータを取得するため、画面の再生成後も変わらずに画面を表示することができます。
しかし、ユーザがアプリをバックグランドにしてAndroid OSからアプリが破棄された後にアプリを再度立ち上げるとどうなるでしょう?
この場合、現在の実装ではデータの永続化を行なっていないため、Webサービスからデータを再取得する必要があります。
これはユーザエクスペリエンスが悪いだけでなく、無駄な通信も発生しています。
Webリクエストをキャッシュするだけで修正可能だと思われますが、さらに問題が発生します。
もし別の種類のユーザデータ(たとえば友人リスト)がリクエストされた場合、キャッシュしていたユーザリストを表示するとユーザの混乱を招きます。
このように間違ったデータを表示しないためにデータをマージする必要があります。
この問題を解消するためにはRoom永続ライブラリを使用します。
Room
Roomは少ない定型コードでデータの永続化を提供するオブジェクトマッピングライブラリです。
コンパイル時に各クエリのバリデーションを行うため、実行時ではなくコンパイル時にエラーを検知することができます。
RoomはSQLテーブルとクエリを処理する基本的な実装を抽象化しています。
また、データベースデータへの変更(コレクションや結合クエリを含む)を監視し、その変更をLiveDataオブジェクトを介して通知することもできます。 さらに、メインスレッド上でストレージにアクセスしないように、スレッド制約を明示的に定義します。
注:すでにSQLite ORMやRealmなどのライブラリを使用している場合は、Roomの機能を必要としない限りは置き換える必要はありません。
Roomを使用するにはローカルスキーマを定義する必要があります。
まず、User
クラスに@Entityを付けて、DBのテーブルであることを示します。
@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
がabstractクラスであることに注目してください。
Roomは自動的に実装を提供するようになっています。詳しくはRoomを参照してください。
ユーザデータをDBにインサートするために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<User>
を返しています。
LiveData
を使用することで、データベースに変更があった時に自動的にオブザーバに対して通知します。
また、オブザーバがいない場合には通知を行わないため効率的です。
Roomのデータを取得するためにUserRepository
を修正します。
@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);
// DBから直接LiveDataを返す
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// バックグランドで処理します。
// データが古いかどうかをチェックします。
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// データを取得
Response response = webservice.getUser(userId).execute();
// TODO エラーチェックなど
// データベースを更新します。
// LiveDataは自動的にリフレッシュされるので、データベースの更新以外に何もする必要はありません
userDao.save(response.body());
}
});
}
}
これで実装が完了しました。
Android OSからアプリが破棄された後に復帰してもユーザデータが永続化されているため、すぐに表示することができます。
UserRepository
ではデータが古くなった場合はバックグランドでデータの更新を行います。
ユーザがPull to RefreshなどでWebサービスからデータを取得する際には通信中であることをUIに表示することによって、ユーザに新鮮なデータを取得していることを示すことが重要です。
これには2つの方法があります。
・getUser
の戻り値のLiveData
データにネットワーク操作のステータスを含めるようにします。
実装例はAppendix:ネットワーク状態を公開するを参照してください。
・リポジトリクラスにUser
の通信中かどうかを返すpublicメソッドを定義します。これはPull to Refreshのようなユーザが明示的に更新するときだけ、ネットワーク状態を画面に表示するのに適しています。
信頼できる唯一の情報源
異なるREST APIが同じデータを返すことはよくあります。
2ペインの画面で両方に友人リストが表示される画面を想像してください。
一方の画面ではAというREST APIにリクエストをし、もう一方の画面ではBと言うREST APIにリクエストします。
AとBのREST APIは異なりますが、同じ友人リストのオブジェクトをレスポンスに含みます。
Aのレスポンスを受け取り、Bがリクエストする最中にサーバ側で友人リストに変更があったとします。
レスポンスの結果を直接UIに反映していた場合、左右の画面に表示されている友人リストは不整合な状態になります。
このため、UserRepository
の実装ではWebサービスからのレスポンスをDBに保存するだけにします。
DBを更新することによって、LiveDataオブジェクトのコールバックがトリガされて、左右どちらの画面も整合性のとれた友人リストを表示することができます。
リポジトリは1つのデータソースを"信頼できる唯一の情報源"として指定することをお勧めします。
テスト
各機能を分離する利点の1つとして、テストが容易になると述べました。
各コードモジュールのテスト方法を見ていきましょう。
ユーザーインターフェイスとインタラクション
UIコードをテストする最良の方法は、Espressoテストを作成することです。
FragmentとViewModelのモックを生成することができます。
フラグメントはViewModelのみ対話するため、UIを完全にテストするには十分です。
ViewModel
ViewModelはJUnit testを使用してテストできます。
テストのためにUserRepository
のモックだけ必要です。
UserRepository
UserRepository
をテストすとには同様にJUnit testを使用します。
Webservice
とDAOのモックが必要です。
適切なWebサービスを呼び出し、その結果をDBに保存し、データのキャッシュが最新のものであれば不要なWebサービスの呼び出しを行わないことをテストできます。
WebサービスとDAOのアクセスがどちらもインターフェースなので、モックにしたり別の実装に置き換えることができます。
UserDao
DAOクラスのテストにはインストルメントテストをお勧めします。
インストルメントテストはUIを必要としないので、高速に動作します。
メモリ上にDBを作成するので、ディスク上のDBファイルを変更するなどのテストに比べて何の影響も受けずに正確なテストを行うことができます。
また、Room
はDB実装の指定できるので、SupportSQLiteOpenHelperを実装したJUnitでテストすることが可能です。
しかし、この方法はホストマシーンと端末のSQLiteのバージョンが異なるのでお勧めしません。
Webservice
テストは外部から切り離すことが重要です。
Webserviceのテストであってもネットワーク通信をさけるべきです。
Webserviceのテストを補助するライブラリはたくさんあります。
例えば、MockWebServerはテストようにモックのローカルサーバを作成するのに役立つ素晴らしいライブラリです。
テストのアーティファクト
Architecture Componentsには、バックグラウンドスレッドを制御するためのmavenアーティファクトが用意されています。
android.arch.core:core-testing
内のアーティファクトには2つのルールがあります。
InstantTaskExecutorRule
このルールを使用して、Architecture Componentが呼び出し元のスレッド上でバックグラウンド操作を即座に実行できるようにすることができます。
CountingTaskExecutorRule
このルールは、インストルメンテーションテストでアーキテクチャコンポーネントのバックグラウンド操作を待つために使用するか、アイドルリソースとしてEspressoに接続します。
最終的なアーキテクチャ
下記の図は、私たちが推奨するアーキテクチャの相関図になります。
従うべき原則
プログラミングは創造的な分野であり、Androidアプリの開発も例外ではありません。
問題を解決するには、複数のActivity、Fragmentでデータのやり取りを行い、オフライン時のためにリモートから取得したデータをローカルに永続化して保持しておくなど、様々な解決方法があります。
以下の推奨事項は強制ではありませんが、私たちの経験では長期的にコードベースをしっかりと固め、テスト可能で、保守も容易になります。
・ マニフェストに定義するエントリポイント、つまりActivity、Service、ブロードキャストレシーバなどはデータソースではありません。
アプリの各コンポーネントはユーザ操作やAndroid OSによっていつ破棄されるかわからないため、全体的なデータは保持せずに関連する部分的なデータだけにしてください。
・ 各モジュールの責任範囲について厳しく線引きしてください。
例えば、ネットワークからデータをロードするコードを複数のクラスやパッケージに分散させて書かないようにしてください。
同様に、データキャッシュや、データバインディングなど機能ごとに分離させて同じクラスに書かないようにしてください。
・ 各モジュールのAPIの公開をできる限定してください。
「一つだけ...」と、内部実装の詳細を公開するようなAPIを作らないようにしてください。
さもなくば、コードをアップデートする度に少なからずとも技術的負債を何度も支払うことになります。
・ モジュール間でやりとりをAPIを定義する場合には、テストのやり方についても考えてください。
例えば、ネットワークからデータを取得するAPIとローカルDBにデータを保存するAPIを1つの場所に混在させたり、基底クラスに定義するとテストが非常に難しくなります。
代わりにこの二つのAPIの定義を明確に分けることによってテストが容易になります。
・ アプリのコア部分だけがアプリによって異なりますが、他の実装については他のアプリでも同じような実装になります。
同じボイラーコードを何度も何度も書いて時間を消費しないでください。
代わりに、 Android Architecture Components や推奨ライブラリを使用してボイラーコードをライブラリに任せて、あなたは自分のアプリのコア部分の実装に集中してください。
・ デバイスがオフラインモードの時でもアプリを使用できるように、できるだけ関連性の高い最新のデータを保存してください。
あなたは高速で安定した通信でアプリを使用できるかもしれませんが、他の人がそうとは限りません。
・ アプリで必要となるデータは1つのデータソースから正のデータとして取得してください。詳しくは(信頼できる唯一の情報源)[#信頼できる唯一の情報源]を参照してください。
Appendix:ネットワーク状態を公開する
推奨されるアプリのアーキテクチャでは、サンプルを簡単にするためにネットワークエラーとローディング状態を意図的に省略しました。
このセクションでは、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の決定木です:
まずはデータのDBを観察(observe)します。
最初にDBからデータがロードされると、NetworkBoundResource
はロードされたデータが十分であるかどうか、またはネットワークからフェッチする必要があるかどうかをチェックします。
ネットワークから更新しながらキャッシュデータを表示する必要があるため、これらの両方が同時に発生する可能性があることに注意してください。
ネットワークコールが正常に完了すると、結果はDBに保存され、ストリームが再初期化されます。
ネットワーク要求が失敗すると、直接エラーを送ります。
以下はNetworkBoundResource
によって提供されてるpublicなAPIです。
// ResultType: リソースデータ
// RequestType: APIレスポンス
public abstract class NetworkBoundResource<ResultType, RequestType> {
// APIのレスポンスをDBに保存する際に呼びます。
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// DBに保存したデータがネットワークからフェッチし直した方が良いかどうか判定する際に呼びます。
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// DBからキャッシュデータを取得する際に呼びます。
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// APIコールを生成する際に呼び出します。
@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() {
}
// リソースデータを表すLiveDataを返します。
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
上記のクラスでは、APIから返されるデータの型とローカルで使用されるデータの型が一致しない可能性があるため、2つ型のパラメータ(ResultType、RequestType)を定義することに注意してください。
また、上記のクラスではネットワークのリクエストにApiResponse
を使用しています。
ApiResponse
は、レスポンスをLiveData
に変換するRetrofit2.Call
クラスのシンプルなラッパーです。
下記が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
を使用してリポジトリでUser
をネットワークとローカルのDBから取得する実装ができるようになりました。
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();
}
}