概要
基本は自分用メモです
Google I/Oでandroid-architecture-componentsが発表されました。
これを利用した設計はAndroidアプリ開発におけるGoogleが推奨する設計の例として挙げられているものになります。
googlesamples/android-architecture-components/GithubBrowserSampleを見てどういう感じなのかまとめてみました。
RxJavaが分かる?方はLiveDataはObservableに置き換えると良いと思います(ただしLiveDataはライフサイクルが考慮されているなどの違いがあります)
またTransformations.switchMapはRxJavaのObservable.swtichMap()のようなものだと解釈しました。
そういう意味でRxJavaについての前提知識が必要になってしまっていると思います。
ツッコミ有ればください。(Twitterでもよいです)
どんなアプリ?
リポジトリ名を入力して検索するとGitHubのAPIを叩いて検索を行い、リポジトリ一覧を表示してくれます。リポジトリの詳細を開いたりもできます。
検索 -> 結果表示までの流れを追ってみよう
具体的にはEditTextで入力してそれに対するリポジトリ一覧が出るまでの流れとなります。
この図を元に説明していきます。 (公式ガイドにある画像になります。
https://developer.android.com/topic/libraries/architecture/guide.html )
Fragment -> ViewModel
SearchFragment.javaがまず表示されます
EditTextにて検索アクションが実行されるとdoSearch()が呼び出されます。
doSearch()では、EditTextの文字を取得してViewModel.setQuery()を呼び出します。
public class SearchFragment extends LifecycleFragment implements Injectable {
...
private void initSearchInputListener() {
binding.get().input.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
doSearch(v);
return true;
}
return false;
});
...
}
private void doSearch(View v) {
String query = binding.get().input.getText().toString();
...
// ここでViewModelへの呼び出し!!
searchViewModel.setQuery(query);
}
ViewModel -> Repository
ViewModel.setQuery()メソッドが呼び出され、ViewModelでは保持していたqueryにsetValueを行い、それを検知してRepositoryへの問い合わせを行い、その結果もLiveDataとなっています。
①②③④の順番で読んでみてください。
public class SearchViewModel extends ViewModel {
// ④ 結果はresultsに格納される。
private final LiveData<Resource<List<Repo>>> results;
private final MutableLiveData<String> query = new MutableLiveData<>();
..
// ① まず、Fragmentから呼び出され、
// LiveDataクラスのインスタンスのqueryに対してsetQueryを行います。
public void setQuery(@NonNull String originalInput) {
String input = originalInput.toLowerCase(Locale.getDefault()).trim();
...
query.setValue(input);
}
...
@Inject
SearchViewModel(RepoRepository repoRepository) {
nextPageHandler = new NextPageHandler(repoRepository);
// ② このqueryの内容が変わったことを検知して、渡しているラムダの実装が実行されます。
results = Transformations.switchMap(query, search -> {
if (search == null || search.trim().length() == 0) {
return AbsentLiveData.create();
} else {
// ③ リポジトリに問い合わせを行います
return repoRepository.search(search);
}
});
}
...
Repository -> DB, API
searchメソッドは以下のようになっています。
NetworkBoundResourceクラスはサンプル内のクラスですが、実装を気にせずに行きましょう。(MediatorLiveDataによる実装など気になるところはありますが、、)
①②③④の順番で読んでいってください
@Singleton
public class RepoRepository {
...
// ① このメソッドはLiveDataを返すことに注目してください。
// つまり変わったことがViewModelに通知(検知)されるということです。
public LiveData<Resource<List<Repo>>> search(String query) {
// ② NetworkBoundResourceを作って.asLiveData()して返しています。
// NetworkBoundResourceの中でうまく処理してくれている部分に関しては今回は触れません。
return new NetworkBoundResource<List<Repo>, RepoSearchResponse>(appExecutors) {
// ⑦ APIアクセスした結果を保存するためのメソッドです。
// Daoのinsertメソッドなどを呼び出しています。
@Override
protected void saveCallResult(@NonNull RepoSearchResponse item) {
List<Integer> repoIds = item.getRepoIds();
RepoSearchResult repoSearchResult = new RepoSearchResult(
query, repoIds, item.getTotal(), item.getNextPage());
db.beginTransaction();
try {
repoDao.insertRepos(item.getItems());
repoDao.insert(repoSearchResult);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
// ④ DBから取得したデータによりAPIへアクセスするかを決定するメソッドです。
// DBから取得したデータがなければ(==null)、APIから取得するようです
@Override
protected boolean shouldFetch(@Nullable List<Repo> data) {
return data == null;
}
// ③ まず最初にDBから取得します。
@NonNull
@Override
protected LiveData<List<Repo>> loadFromDb() {
return Transformations.switchMap(repoDao.search(query), searchData -> {
if (searchData == null) {
return AbsentLiveData.create();
} else {
return repoDao.loadOrdered(searchData.repoIds);
}
});
}
// ⑤ APIから取得します。
@NonNull
@Override
protected LiveData<ApiResponse<RepoSearchResponse>> createCall() {
return githubService.searchRepos(query);
}
// ⑥ APIから取得したResponseを整理します。
@Override
protected RepoSearchResponse processResponse(ApiResponse<RepoSearchResponse> response) {
// responseを整頓します。
RepoSearchResponse body = response.body;
if (body != null) {
body.setNextPage(response.getNextPage());
}
return body;
}
}.asLiveData();
}
Repository -> DBに着目してみてみる
まずrepoDao.search(query)からLiveDataを取得して、その結果を使います。
その結果はsearchDataとして流れてきます。
それをさらにrepoDao.loadOrdered(searchData.repoIds)を呼ぶ出して、LiveDataで返します。
@Singleton
public class RepoRepository {
...
@NonNull
@Override
protected LiveData<List<Repo>> loadFromDb() {
return Transformations.switchMap(repoDao.search(query), searchData -> {
if (searchData == null) {
return AbsentLiveData.create();
} else {
return repoDao.loadOrdered(searchData.repoIds);
}
});
}
DB -> SQLiteに着目してみる
RepoDaoクラスは以下のようになっています。
@Queryを使ってSQLiteにSQL文でアクセスしています。これはRoomというAndroid Architecture Componentの機能によるものです。
queryを元にRepoSearchResultというテーブルから検索を行い返します。(LiveDataなので、取得でき次第渡されます。)
@Dao
public class RepoDao {
...
// ① Repositoryから呼び出され、Roomの機能によりRepoSearchResultからqueryで検索
@Query("SELECT * FROM RepoSearchResult WHERE query = :query")
public abstract LiveData<RepoSearchResult> search(String query);
// ①の結果を元にRepositoryから呼び出されます。
// 検索するリポジトリidをSparseIntArrayに入れておき、
// loadByIdで取得後、結果をソートして返します。
public LiveData<List<Repo>> loadOrdered(List<Integer> repoIds) {
SparseIntArray order = new SparseIntArray();
int index = 0;
for (Integer repoId : repoIds) {
order.put(repoId, index++);
}
// ここでloadById(repoIds)を使う
return Transformations.map(loadById(repoIds), repositories -> {
Collections.sort(repositories, (r1, r2) -> {
int pos1 = order.get(r1.id);
int pos2 = order.get(r2.id);
return pos1 - pos2;
});
return repositories;
});
}
@Query("SELECT * FROM Repo WHERE id in (:repoIds)")
protected abstract LiveData<List<Repo>> loadById(List<Integer> repoIds);
Repository -> Remote Data Sourceに注目してみる
呼び出しているだけですね。
@Singleton
public class RepoRepository {
@NonNull
@Override
protected LiveData<ApiResponse<RepoSearchResponse>> createCall() {
return githubService.searchRepos(query);
}
Remote Data Source -> API
基本はRetrofitでのアクセスで、android-architecture-componentsはRetrofit用のAdapterを用意しているため、以下のようにLiveDataで返せるようになっています。
@GET("search/repositories")
LiveData<ApiResponse<RepoSearchResponse>> searchRepos(@Query("q") String query);
SearchViewModel -> Fragment
repoRepositoryからLiveDataを返してresultsをメンバ変数に入っていました。
そして、getResultsによりFragmentに渡っています。
public class SearchViewModel extends ViewModel {
private final LiveData<Resource<List<Repo>>> results;
...
@Inject
SearchViewModel(RepoRepository repoRepository) {
nextPageHandler = new NextPageHandler(repoRepository);
results = Transformations.switchMap(query, search -> {
if (search == null || search.trim().length() == 0) {
return AbsentLiveData.create();
} else {
return repoRepository.search(search);
}
});
}
LiveData<Resource<List<Repo>>> getResults() {
return results;
}
Fragment -> View
LiveData.observe()により、Viewにデータを反映しています。(DataBindingを利用)
searchViewModel.getResults().observe(this, result -> {
binding.get().setSearchResource(result);
binding.get().setResultCount((result == null || result.data == null)
? 0 : result.data.size());
adapter.get().replace(result == null ? null : result.data);
binding.get().executePendingBindings();
});
まとめ
良かったところ:
ユーザーからのクエリの入力や、Repositoryにアクセスした結果を、ViewModelでのみLiveDataをメンバ変数で持つようにすることで、ViewModelを見れば表示できると言う形にしていて、割りとわかりやすそうでした。
またRoomなどはとてもシンプルにかけて、またViewModelによる管理もシンプルに見えており、いいところがあるかなと思いました。
ちょっと微妙??:
ただLiveDataによる書き方により、メソッドチェーンでない書き方でswtichMapを書いたり、今回紹介していませんでしたがスレッドの指定の部分をNetworkBoundResourceのようなクラスに分離して独自で書いて自分でコールバックを実装していたりするので、RxJavaのほうが見通しが良さそうに見えました。(個人の感想です)
自分も調べ始めたばかりで未熟で、分かっていないところがたくさんあります。なのでつっこみをください。
Architecture Components自体もまだalpha1なので、今後の動向が楽しみですね!