- HTTPクライアントには Retrofit を使うようにしました。
- データバインディングの使い方と、RxJava で非同期処理についての再度勉強しました。RxJava と LiveData の連携についても確認
ライブラリ
- RxJava:https://github.com/ReactiveX/RxJava
- Retrofit:https://github.com/square/retrofit
- Moshi:https://github.com/square/moshi
- OkHttp:https://github.com/square/okhttp
とは
- RxJava とは
- Javaでリアクティブプログラミングを行うためのライブラリ
- https://codezine.jp/article/detail/9570
- OkHttp とは
- HTTP通信とSPDY通信をするためのクライアント用ライブラリ
- Retrofit とは
- 型安全な Android 向けの HTTP クライアントライブラリです。正確には OkHttp のラッパーで、アノテーションなどを使ってより実装しやすくするためのライブラリです。
- Moshi とは
- Moshiとは、JavaやAndroid向けのモダンなJSONライブラリーです。また、JSONからJavaのオブジェクトへの変換が簡単に行なえます。
- LiveDataとは
- 監視可能なデータホルダー クラスです。通常の監視とは異なり、LiveData はライフサイクルに応じた監視が可能です。つまり、アクティビティ、フラグメント、サービスなどの他のアプリ コンポーネントのライフサイクルが考慮されます。このため、LiveData はライフサイクルの状態がアクティブなアプリ コンポーネント オブザーバーのみを更新します。
- https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
- ViewModel とは
- ViewModel は、ライフサイクルを意識した方法で UI 関連のデータを保存し管理するためのクラスです。ViewModel クラスを使用すると、画面の回転などの構成の変更後にデータを引き継ぐことができます。
- https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
注意
- ここから先は、実装ポイントです。下記手順で実装したわけではありません。
- 実装内容は、https://github.com/ka-yamao/example/tree/3595 のブランチを参考に
作成したサンプルアプリ
build.gradle の設定
プロジェクト直下の build.gradle に下記を追加
- Kotlin を使うので kotlin-gradle-plugin を追加
build.gradle
buildscript {
ext {
kotlin_version = '1.5.21'
}
// 省略
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
// Kotlin 追加
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
app/build.gradle にプラングインを適用
- kotlin-android の plugin を追加
- kotlin-android-extensions は不要となった。
- kotlin-kapt は、Javaの Pluggable Annotation Processing API を Kotlin で使えるようにするためのプラグインです。今のところ使わない。https://qiita.com/uhooi/items/836902cdd322f9accded
app/build.gradle
apply plugin: 'kotlin-android' // 追加
// apply plugin: 'kotlin-android-extensions' // 1.4.20-M2 で deprecated になった 追加しなくてもOK
// apply plugin: 'kotlin-kapt' ※今のところ使わない https://qiita.com/uhooi/items/836902cdd322f9accded
app/build.gradle データバインディングの設定を追加
- android の括りに下記を追加
android {
// 省略
buildFeatures {
dataBinding true
}
// 省略
}
app/build.gradle にライブラリを追加
- Moshi, Retrofit, RxJava を追加
- Kotlin 1.4.0以降、
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
の記述は不要になったみたい
app/build.gradle
dependencies {
// 省略
// Moshi
def moshi_version = '1.12.0'
implementation "com.squareup.moshi:moshi:$moshi_version"
implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.github.akarnokd:rxjava3-retrofit-adapter:3.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
// RxJava
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
implementation "io.reactivex.rxjava3:rxjava:3.1.0"
// 省略
}
Retrofit, Moshi, okhttp3 まわり
Retrofit APIサービス(interface)
- API のインターフェースを作成します。
@GET("pokemon/")
ベースURL以降のパスを設定している- 戻り値は、RxJava を使うので Observable に
- 引数ではパラメータを指定しています。
@
アノテーションでいろいろ指定できます。 リクエストメソッド、HTTPヘッダー、パラメーターなどなど、詳しくはこちら Retrofit
src/main/java/c/local/com/example/PokeAPIService.java
public interface PokeAPIService {
@GET("pokemon/")
Observable getPokemons(@QueryMap Map<String, String> query);
}
Retrofit の具象クラス
- PokeAPIService を実装したクラスです。
- ベースURLの設定、パーサー、HTTPヘッダー操作などをしています。
src/main/java/c/local/com/example/NetworkModule.java
public class NetworkModule {
/**
* PokeAPIService の具象クラス
* @return PokeAPIService
*/
public static PokeAPIService providePokemonApiService() {
Moshi moshi = new Moshi.Builder().build();
return new Retrofit.Builder()
.baseUrl("https://pokeapi.co/api/v2/") // ベースURLの設定
.addConverterFactory(MoshiConverterFactory.create(moshi)) // JSON パーサーに Moshi を設定
.addCallAdapterFactory(RxJava3CallAdapterFactory.createAsync()) // Rxjava を利用するので設定
.client(createHttpClient()) // HTTPクライアントを設定
.build()
.create(PokeAPIService.class); // APIのインターフェースを設定
}
/**
* HTTPヘッダーを操作、URLのログを出力するため
*
* @return
*/
private static OkHttpClient createHttpClient() {
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain -> {
Request original = chain.request();
DLog.d(TAG, original.url().toString());
//header設定
Request request = original.newBuilder()
.header("Accept", "application/json")
.method(original.method(), original.body())
.build();
okhttp3.Response response = chain.proceed(request);
return response;
});
return httpClient.build();
}
}
RxJava から Retrofit を使う
- 汚い実装だけど、こんな感じで実装
- DataRepository.java が生成されるタイミングで PublishSubject を生成
- RetrofitViewModel.java に MediatorLiveData のLiveData があります。
- RetrofitViewModel.java のインスタンスを生成するとき、DataRepository 側で subscribe する処理を実装しています。このとき MediatorLiveData を引数で渡しています。
LiveDataを渡して、DataRepository 側で subscribe する
src/main/java/c/local/com/example/viewmodel/RetrofitViewModel.java
// コンストラクタ
public RetrofitViewModel(@NonNull Application application,
@NonNull SavedStateHandle savedStateHandle) {
super(application);
// Repository の生成
mRepository = ((BasicApp) application).getRepository();
// LiveData を渡して、Observable を生成し、subscribe で LiveData を更新する
Disposable disposable = mRepository.createObservableSubscribe(mPokemonListLiveData, mPokemonListInfoLiveData);
compositeDisposable.add(disposable);
}
PublishSubject で API を呼び出し、サブスクライブ
src/main/java/c/local/com/example/DataRepository.java
/**
* ポケモンのリストを取得
*
* @param pokemonList
* @param pokemonListInfo
* @return
*/
public Disposable createObservableSubscribe(MediatorLiveData pokemonList, MediatorLiveData pokemonListInfo) {
return mPublishSubject
// 1000ミリ秒間の間で最後の値を流す
.throttleLast(1000, TimeUnit.MILLISECONDS)
// 入出力用のスレッド
.subscribeOn(Schedulers.io())
// 非同期の解決順で並び、APIで Observable を取得
.concatMap(info -> mApiService.getPokemons(info.toQueryMap()), 1)
// メインスレッド
.observeOn(AndroidSchedulers.mainThread())
// サブスクライブ リストを生成して、LiveData へ設定
.subscribe(result -> {
// ポケモンのリスト作成
List list = pokemonList.getValue();
if (result.previous == null) {
list = result.toPokemonList();
} else {
list.addAll(result.toPokemonList());
}
// ポケモンリストを LiveDataへ設定 ※メインスレッドなので setValue
pokemonList.setValue(list);
// リストのページ情報
PokemonListInfo info = new PokemonListInfo(result.count, result.next);
// リストのページ情報を LiveDataへ設定 ※メインスレッドなので setValue
pokemonListInfo.setValue(info);
},
error -> {
// ※省略
}, () -> {
// ※省略
});
}
PublishSubject で onNext をしてポケモンリスト取得
src/main/java/c/local/com/example/viewmodel/RetrofitViewModel.java
/**
* ポケモンリストの取得、追加読み込み
*/
public void fetch(boolean isAdd) {
mRepository.fetch(isAdd ? mPokemonListInfoLiveData.getValue() : new PokemonListInfo());
}
src/main/java/c/local/com/example/DataRepository.java
public void fetch(PokemonListInfo pokemonListInfo) {
// onNext で ページ情報を流す
mPublishSubject.onNext(pokemonListInfo);
}
ポケモンリストを Retrofit, Rxjava で取得するのは、だいたいこんな感じです。
Rxjava PublishSubject, PublishProcessor の確認
- 連続でAPIリクエストをする処理があるとき、負荷対策をどうするか試してみた。試したのは、2パターン
- PublishSubject で
throttleLast(1000, TimeUnit.MILLISECONDS)
を使う - PublishProcessor で
onBackpressureLatest()
を使い、さらにPublishSubject でthrottleLast(1000, TimeUnit.MILLISECONDS)
を使う
結果
- どちらもかわならい
実装内容
src/main/java/c/local/com/example/viewmodel/RxJavaViewModel.java
public class RxJavaViewModel extends AndroidViewModel {
private MediatorLiveData mLogListLiveData = new MediatorLiveData<>();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
// 検索のサブジェクト
private PublishSubject mPublishSubject;
// 検索実行のプロセル管理
public PublishProcessor mPublishProcessor;
// API
private PokeAPIService mApiService;
public RxJavaViewModel(@NonNull Application application,
@NonNull SavedStateHandle savedStateHandle) {
super(application);
mLogListLiveData = new MediatorLiveData<>();
mLogListLiveData.setValue(new ArrayList<>());
mApiService = BasicApp.getApp().getApi();
// PublishProcessor を生成
mPublishProcessor = PublishProcessor.create();
mPublishProcessor.onBackpressureLatest().observeOn(Schedulers.from(Executors.newCachedThreadPool()), false, 1).subscribe(p -> {
log("processor");
mPublishSubject.onNext(p);
});
// PublishSubject を生成
Disposable disposable = createObservable();
// Dispose を追加
compositeDisposable.add(disposable);
}
/**
* PublishSubject を生成、subscribe しておく
*
* @return
*/
public Disposable createObservable() {
mPublishSubject = PublishSubject.create();
return mPublishSubject
.throttleLast(1000, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.io())
.concatMap(info -> {
log("subject");
return mApiService.getPokemons(info.toQueryMap());
}, 1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
log("success");
},
error -> {
log("error" + error.getMessage());
}, () -> {
log("complete");
});
}
/**
* ボタンの別の処理
*
* @param index
*/
public void fetch(int index) {
log("index :" + index);
switch (index) {
case 0:
// PublishProcessor & PublishSubject
log("onNext");
mPublishProcessor.onNext(new PokemonListInfo());
break;
case 1:
// PublishSubject
log("onNext");
mPublishSubject.onNext(new PokemonListInfo());
break;
case 2:
// エラー
log("onNext");
mPublishSubject.onNext(new PokemonListInfo(0, "sdss"));
break;
case 3:
// PublishProcessor 10回連打
for (int i = 0; i < 10; i++) {
log("onNext " + i);
mPublishProcessor.onNext(new PokemonListInfo());
}
break;
case 4:
// PublishSubject 10回連打
for (int i = 0; i < 10; i++) {
log("onNext " + i);
mPublishSubject.onNext(new PokemonListInfo());
}
break;
case 999:
// ログのクリア
mLogListLiveData.postValue(new ArrayList<>());
break;
}
}
/**
* ログを LiveData へ追加
*
* @param log
*/
public void log(String log) {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("hh時mm分ss秒SSSミリ秒 ");
String l = sdf.format(calendar.getTime()) + " : " + log;
List logs = mLogListLiveData.getValue();
logs.add(0, l);
mLogListLiveData.postValue(logs);
}
public MediatorLiveData getLogListLiveData() {
return mLogListLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
}
課題
- RxJava のバージョン 2系のとき、同時リクエスト制御する処理を入れていたが、RxJava の3系での実装方法がよくわからない。
- 今後時間を見つけて、
.replay(1)
や.refCount();
の調査をしたい
source(3595 branch)
- https://github.com/ka-yamao/example/tree/3595
参考サイト
- 【Android】はじめてのDataBinding
- Hilt、RxJava 3、レトロフィット、ルーム、ライブデータ、ビューバインディングを備えたMVVM
- 大量に流れてくるデータを「間引き」する debounce
- 複数のストリームの「どれか」が変わったら通知する combineLatest
- RxJava は Subscriber を中心に捉えると理解しやすいんじゃないかという話
- 【Programming】RxJava リアクティブプログラミング vol.3 / RxJavaの構成~前編~