これらの一連の実装をどうするか考えていた。
前提
- REST APIを使っている
- APIはデータモデル+メタデータを返す
- モデルはメモリキャッシュ、ファイルキャッシュにも対応する
モデルとレスポンスの形式
メタデータの中にはページ情報など、サーバ/クライアントでデータをやりとりするのに必要なデータが入っている。
メタデータはモデルの外に持たせたいので、モデル層をResponseクラスでラップした。ここにページネーションのしくみを持たせることにする。
public class Response<T> {
private T result;
private Extra extra;
private Observable<Response<T>> next;
public T getResult() {
return result;
}
public Extra getExtra() {
return extra;
}
public int nextPage() {
return extra.getLinks().getNext().getPage();
}
public Observable<Response<T>> getNext() {
return next;
}
Response#getNext
で次ページにリクエストできる参照が得られるようなイメージだ。
モデルへのアクセス
サービスクラスを作ってモデルの取得先の抽象化をする。
new RecipeService().search("tapas", 1); // どこかからかモデルが返ってくる
RecipeService#search
は(今は実装していないが)キャッシュにヒットしなかったらAPIを叩いて結果を返すようにする。
レスポンスを受けたときに次のページのObservableを作ってセットするようにした。
これによって、リストの最後までスクロールしたときに Response#getNext
で次のページを要求できるようにしている。
public Observable<Response<List<Recipe>>> search(final String keyword, final int page) {
String path = String.format("/recipes?keyword=%s&page=%s", keyword, page);
return request(Method.GET, path, null, null, new TypeToken<Response<List<Recipe>>>() {
}).map(new Func1<Response<List<Recipe>>, Response<List<Recipe>>>() {
@Override
public Response<List<Recipe>> call(Response<List<Recipe>> r) {
r.setNext(search(keyword, r.nextPage()));
return r;
}
});
}
public <T> Observable<T> request(Method method, String path, Map<String, String> headers, RequestBody body, TypeToken<T> type) {
return Observable.create(new RequestSubscriber<>(method, path, headers, body, type))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
補足をすると、AndroidはJava 7で書くことができる、というかJava 8は使えないので、ラムダではなくFunc1やAction1などを継承して使っている。
ただし Retrolambda のようなバックポーティングツールを使うことでラムダなどの一部機能を使うこともできる。
リストとデータのアダプター
最初にControllerからkeywordを渡してアダプターを初期化する。
SearchResultAdapter searchResultAdapter = new SearchResultAdapter(this);
searchResultAdapter.search("tapas");
アダプターはキーワードを受け取ったらSubjectを生成する。これはあとでイベントの発火に使う。
レスポンスを受け取ったときには次のページのObservableを持っているので、それも保持するようにしておく。
public void search(String keyword) {
responseSubject = BehaviorSubject.create(new RecipeService().search(keyword, 1));
responseSubject.flatMap(new Func1<Observable<Response<List<Recipe>>>, Observable<Response<List<Recipe>>>>() {
@Override
public Observable<Response<List<Recipe>>> call(Observable<Response<List<Recipe>>> r) {
return r;
}
}).subscribe(new Action1<Response<List<Recipe>>>() {
@Override
public void call(Response<List<Recipe>> r) {
recipes.addAll(r.getResult());
notifyDataSetChanged();
pagedResponse = r.getNext();
}
});
}
次のページの要求
末端までスクロールしたときに発火する。
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
...
if (recipes.size() - 1 == i) {
responseSubject.onNext(pagedResponse);
}
雑感
RxJavaが最近流行ってるっぽい(RxJava Night #rxjnight - connpass)ので導入するか判断するために、実用的なユースケースを挙げてそれが実装できるか検討している。
RxJavaを使うことでAsyncTaskLoaderなどのボイラープレートを書く作業から解放されるだけでなく、データやイベントを抽象的にすることによって扱いやすくなったり、本質の処理を書くことに集中できるようになることを期待している。
一方で、RxJavaを導入したことによる新しい複雑さにどう対応するかということも考えている。
昨日RxJavaの知見を求めてインターネットを泳いでいたら、RxJavaを導入したらeerie problems and sleepless nightsになったという 記事 を見つけて震えていた。
たぶん上記のコードも適切にunsubscribeしないとどこかメモリリークしていると思う。
今までもいくつかのライブラリやフレームワークを導入して失敗したことがあるので、今回も慎重に検討を進めているが、好奇心に負けて導入してしまいそうな気がする。