Android RxJava と Retrofit
このチュートリアルでは、AndroidアプリでRxJavaを使用してRetrofitの呼び出しを実装します。RetrofitとRxJavaを使用して、RecyclerViewを埋めるアプリケーションを作成します。CryptoCurrency APIを使用する予定です。
何を学びますか? (Nani o manabimasu ka?)
- Making a Retrofit call using RxJava.
- Doing Multiple Retrofit Calls Using RxJava
- Transforming the Retrofit POJO response using RxJava
私たちはAndroidアプリケーションでJava 8を使用し、ラムダ式を活用する予定です。
概要
Retrofitは、RESTクライアントであり、OkHttpをHttpClientとして使用し、レスポンスの解析にJSONパーサーを使用します。ここでは、gsonをJSONパーサーとして使用します。Retrofitインスタンスの作成方法は次のようになります。
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
Gson gson = new GsonBuilder()
.setLenient()
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
ネットワークコール中のデータをログに記録するためには、HttpLoggingInterceptorを使用します。RxJavaは、ストリーム形式の非同期およびリアクティブなプログラミングに使用されるライブラリです。RxJavaでは異なるスレッドを使用します。ネットワークコールにはバックグラウンドスレッドを、UIの更新にはメインスレッドを使用します。RxJavaでは、スケジューラが異なるスレッドを使用して操作を実行する役割を果たしています。RxAndroidはRxJavaの拡張であり、Android環境で使用するためのAndroidスレッドが含まれています。Retrofit環境でRxJavaを使用するには、2つの主な変更を行う必要があります。
- Add the RxJava in Retrofit Builder.
- Use Observable type in the interface instead of Call
複数のコールを実行したり、レスポンスを変換するためには、RxJavaのオペレータを使用します。以下のサンプルアプリケーションを通して、その方法を見てみましょう。
プロジェクトの構造
私たちのbuild.gradleファイルに次の依存関係を追加してください。 (Watashitachi no build.gradle faini jyunjo kankei o tsuika shitekudasai)
implementation 'com.android.support:cardview-v7:27.1.0'
implementation 'com.android.support:design:27.1.0'
implementation('com.squareup.retrofit2:retrofit:2.3.0')
{
exclude module: 'okhttp'
}
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
コード
以下に、layout activity_main.xml のコードが示されています。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>
以下には、CryptocurrencyService.javaクラスのコードが示されています。
package com.scdev.rxjavaretrofit;
import com.scdev.rxjavaretrofit.pojo.Crypto;
import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface CryptocurrencyService {
String BASE_URL = "https://api.cryptonator.com/api/full/";
@GET("{coin}-usd")
Observable<Crypto> getCoinData(@Path("coin") String coin);
}
@Pathは、中括弧に指定したパスを渡します。注意:@Pathパラメータの名前は、@GET内の名前と一致する必要があります。以下に、POJOクラスであるCrypto.javaが示されています。
package com.scdev.rxjavaretrofit.pojo;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class Crypto {
@SerializedName("ticker")
public Ticker ticker;
@SerializedName("timestamp")
public Integer timestamp;
@SerializedName("success")
public Boolean success;
@SerializedName("error")
public String error;
public class Market {
@SerializedName("market")
public String market;
@SerializedName("price")
public String price;
@SerializedName("volume")
public Float volume;
public String coinName;
}
public class Ticker {
@SerializedName("base")
public String base;
@SerializedName("target")
public String target;
@SerializedName("price")
public String price;
@SerializedName("volume")
public String volume;
@SerializedName("change")
public String change;
@SerializedName("markets")
public List<Market> markets = null;
}
}
coinNameは、私たちが設定したフィールドです。RxJavaの魔法を使って、このフィールドに値を設定して、レスポンスを変換します。
RxJavaを使用して単一のコールを作成する
CryptocurrencyService cryptocurrencyService = retrofit.create(CryptocurrencyService.class);
Observable<Crypto> cryptoObservable = cryptocurrencyService.getCoinData("btc");
cryptoObservable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(result -> result.ticker)
.subscribe(this::handleResults, this::handleError);
subscribeOn() は、ネットワーク呼び出しを行うためにスケジューラスレッドを作成します。以下のスケジューラのいずれかを渡すことができます。
- trampoline(): This runs the tasks on the current thread. So it’ll run your code after the current task on the thread is complete. Useful for queueing operations.
- newThread(): Creates and returns a Scheduler that creates a new Thread for each unit of work. This is costly since it creates a separate thread everytime.
- computation(): Creates and returns a Scheduler intended for computational work. This should be used for parallel work since the thread pool is bound. I/O operations shouldn’t be done here.
- io(): Creates and returns a Scheduler intended for IO-bound work. Again it’s bounded like computation. Typically this is used for network calls.
subscribeOn()とobserveOn()の違い
- subscribeOn works downstream and upstream. All the tasks above and below it would use the same thread.
- observeOn works downstream only.
- consecutive subscribeOn methods won’t change the thread. Only the first subscribeOn thread would be used.
- consecutive observeOn methods will change the thread.
- After an observeOn(), putting a subscribeOn() won’t change the thread. Hence observeOn should generally come after a subscribeOn.
AndroidSchedulers.mainThread()はRxAndroidの一部であり、データをメインスレッド上でのみ観察するために使用されます。subscribeメソッドはretrofitの呼び出しをトリガーとし、handleResultsメソッド内でデータを取得します。
複数の電話
RxJavaのmergeオペレータを使用して、Retrofitの呼び出しを2つ連続して行います。
Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc");
Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth");
Observable.merge(btcObservable, ethObservable)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResults, this::handleError);
返答を変えるようにする
POJOのレスポンスを変換するために、以下の方法があります。
Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc")
.map(result -> Observable.fromIterable(result.ticker.markets))
.flatMap(x -> x).filter(y -> {
y.coinName = "btc";
return true;
}).toList().toObservable();
Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth")
.map(result -> Observable.fromIterable(result.ticker.markets))
.flatMap(x -> x).filter(y -> {
y.coinName = "eth";
return true;
}).toList().toObservable();
Observable.merge(btcObservable, ethObservable)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResults, this::handleError);
私たちは、Observable.fromIterableを使用してマップの結果をObservableストリームに変換します。flatMapは要素ごとに作用します。したがって、ArrayListを単一の要素に変換します。filterメソッドでは応答を変更します。toList()はflatMapの結果をListに変換するために使用されます。toObservable()はそれらをObservableストリームとしてラップします。
メインアクティビティ.java
下記はMainActivity.javaクラスのコードです。
package com.scdev.rxjavaretrofit;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.scdev.rxjavaretrofit.pojo.Crypto;
import java.util.List;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import static com.scdev.rxjavaretrofit.CryptocurrencyService.BASE_URL;
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
Retrofit retrofit;
RecyclerViewAdapter recyclerViewAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerViewAdapter = new RecyclerViewAdapter();
recyclerView.setAdapter(recyclerViewAdapter);
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
Gson gson = new GsonBuilder()
.setLenient()
.create();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
callEndpoints();
}
private void callEndpoints() {
CryptocurrencyService cryptocurrencyService = retrofit.create(CryptocurrencyService.class);
//Single call
/*Observable<Crypto> cryptoObservable = cryptocurrencyService.getCoinData("btc");
cryptoObservable.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).map(result -> result.ticker).subscribe(this::handleResults, this::handleError);*/
Observable<List<Crypto.Market>> btcObservable = cryptocurrencyService.getCoinData("btc")
.map(result -> Observable.fromIterable(result.ticker.markets))
.flatMap(x -> x).filter(y -> {
y.coinName = "btc";
return true;
}).toList().toObservable();
Observable<List<Crypto.Market>> ethObservable = cryptocurrencyService.getCoinData("eth")
.map(result -> Observable.fromIterable(result.ticker.markets))
.flatMap(x -> x).filter(y -> {
y.coinName = "eth";
return true;
}).toList().toObservable();
Observable.merge(btcObservable, ethObservable)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResults, this::handleError);
}
private void handleResults(List<Crypto.Market> marketList) {
if (marketList != null && marketList.size() != 0) {
recyclerViewAdapter.setData(marketList);
} else {
Toast.makeText(this, "NO RESULTS FOUND",
Toast.LENGTH_LONG).show();
}
}
private void handleError(Throwable t) {
Toast.makeText(this, "ERROR IN FETCHING API RESPONSE. Try again",
Toast.LENGTH_LONG).show();
}
}
handleResultsとhandleErrorは、Java 8のinvocation ::を使用して呼び出されます。handleResultsでは、変換されたレスポンスをReyclerViewAdapterに設定します。もしレスポンスにエラーがある場合は、handleError()が呼び出されます。recyclerview_item_layoutのレイアウトのコードは以下の通りです。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.CardView
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
android:id="@+id/txtCoin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:textAllCaps="true"
android:textColor="@android:color/black"
app:layout_constraintHorizontal_bias="0.023"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtMarket"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.025"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtCoin" />
<TextView
android:id="@+id/txtPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.025"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtMarket" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
RecyclerViewAdapter.javaクラスのコードは次のようになります。
package com.scdev.rxjavaretrofit;
import android.graphics.Color;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.scdev.rxjavaretrofit.pojo.Crypto;
import java.util.ArrayList;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
private List<Crypto.Market> marketList;
public RecyclerViewAdapter() {
marketList = new ArrayList<>();
}
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recyclerview_item_layout, parent, false);
RecyclerViewAdapter.ViewHolder viewHolder = new RecyclerViewAdapter.ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, int position) {
Crypto.Market market = marketList.get(position);
holder.txtCoin.setText(market.coinName);
holder.txtMarket.setText(market.market);
holder.txtPrice.setText("$" + String.format("%.2f", Double.parseDouble(market.price)));
if (market.coinName.equalsIgnoreCase("eth")) {
holder.cardView.setCardBackgroundColor(Color.GRAY);
} else {
holder.cardView.setCardBackgroundColor(Color.GREEN);
}
}
@Override
public int getItemCount() {
return marketList.size();
}
public void setData(List<Crypto.Market> data) {
this.marketList.addAll(data);
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView txtCoin;
public TextView txtMarket;
public TextView txtPrice;
public CardView cardView;
public ViewHolder(View view) {
super(view);
txtCoin = view.findViewById(R.id.txtCoin);
txtMarket = view.findViewById(R.id.txtMarket);
txtPrice = view.findViewById(R.id.txtPrice);
cardView = view.findViewById(R.id.cardView);
}
}
}
上記のアプリケーションの実行結果は以下の通りです: 上記の出力は、ビットコインとイーサリアムの市場価格の結果をRetrofitを通じてマージしています。ここで、チュートリアルは終了です。以下のリンクからAndroid RxJavaRetrofitプロジェクトをダウンロードできます。
RxJavaRetrofitを日本語で言い換えると、一つのオプションのみが必要です。