Android の Java プロジェクトに Kotlin, Rxjava, Retrofit, Moshi, OkHttp を追加

  • 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

参考サイト