はじめに
皆さん、ごきげんよう!れぶです!
先日Retrofit2
ライブラリを使って、API通信した結果をRecyclerView
でリスト表示しました。その際に自身が悩んだ箇所が1点あったので、今回の記事で原因と解決方法を記述していきます。
自身のためでもありますが、自分と同じ悩みを持つ方々にも刺さる記事になれば幸いです。
それでは、参りましょう!!
開発環境
- MacBook Air
- Android Studio Bumblebee | 2021.1.1 Patch 1
- Java 8
- compileSdkVersion 31
- minSdkVersion 21
動作イメージ
完成イメージは以下になります。
入力された郵便番号をもとにAPI通信(zipcloudさんのAPIを利用)を行い、町域名をRecyclerViewでリスト表示するサンプルプログラムになります。
悩んだ箇所
Searchview
(アプリ一番上の入力欄)に7桁の郵便番号を入力しEnterキーを押しても、一回でリアルタイムに反映されない現象が起きました。
具体的には以下の流れになります。
⑴ 実在する7桁の郵便番号を入力しEnterキーを押す▶️反映されない
⑵ 再度、実在する7桁の郵便番号を入力しEnterキーを押す▶️⑴で入力した郵便番号の町域名が反映される
つまり、見た目的に1回分ずれてRecyclerviewにデータが反映されてしまいます。
原因
色々考えた結果、以下のgetAddress
メソッド内に問題があることを発見しました。
CityRepository.java
(一部省略)ではAPI通信を行い、取得したデータをgetAddress
メソッドで返しています。
private ApiInterface service;
private List<Address> address;
public List<Address> getAddress(String zipcode) {
//APIを呼び出し
service.getCity(zipcode).enqueue(new Callback() {
//リクエスト成功時
@Override
public void onResponse(Call call, Response response) {
Log.i("message2", "Successed to request");
//通信結果を受け取る
City city = (City) response.body();
address = city.getResults();
}
@Override
public void onFailure(Call call, Throwable t) {
Log.i("message2", "error:" + t);
}
});
Log.i("message2", "Successed to request2");
return address;
}
ログを見ると、「Successed to request2」→「Successed to request」の順で出力してしまいます。(想定してた処理実行の順番は逆)。
つまり、Call.enqueue()
が非同期で処理を行うメソッドであることが、今回の現象の根本的な原因でした。非同期処理は上から順には処理されません。上記コードではCall.enqueue()
よりも先にreturn文が実行されます。
1回分ずれて表示されてしまう流れは以下になります。
❶ ユーザーアクション(1回目の郵便番号の入力)
❷ 前回のaddress(初回はなし)をreturn文で返す
❸ ❷をView側に反映する
❹ Call.enqueue()が実行され、addressに値がセットされる
❺ ユーザーアクション(2回目の郵便番号の入力)
❻ ❹をreturn文で返す
❼ ❻をView側に反映する
解決方法
結論、Call.execute()
を使うことです。
Call.enqueue()
が非同期で処理されてしまうのであれば、Call.execute()
で同期処理を行えば処理実行の順番ずれは起きないという考えです。すると、ずれがなくRecyclerviewにデータが反映されました。
private ApiInterface service;
private static final int NUMBER_OF_THREADS = 4;
public static final ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public List<Address> getAddress(String zipcode) {
try {
//通信結果を受け取る
Response<City> response = service.getCity(zipcode).execute();
if(response.isSuccessful()) {
City city = response.body();
List<Address> address = city.getResults();
return address;
} else {
Log.d("message2", "error_code" + response.code());
}
} catch (IOException e) {
Log.i("message2", "error:" + e.getMessage());
}
return null;
}
処理の流れは以下になります。
❶ ユーザーアクション(郵便番号の入力)
❷ Call.execute()が実行され、結果を受け取る
❸ ❷をreturn文で返す
❸ ❸をView側に反映する
上記コードでは、同期処理なので勿論上から処理されます。
ただし、Call.execute()
の実行はメインスレッドではできません。メインスレッドとは別にスレッドを立てて、その中で実行します。以下のコードでは、ViewModel
クラス内でexecutorService.execute()
を使って非同期的に実行しています。(今回は、Repository(Model)
で取得した値をViewModel
経由でView
に反映)
※因みにCall.enqueue()
は元々非同期メソッドであるため、別にスレッドを立てて実行する必要はありません。
public class CityViewModel extends AndroidViewModel {
private MutableLiveData<List<Address>> address = new MutableLiveData<List<Address>>();
private CityRepository repository;
public CityViewModel(@NonNull Application application) {
super(application);
repository = new CityRepository();
}
public LiveData<List<Address>> getAddress() { return address; }
//取得したデータを設定
//非同期で実行
public void searchAddress(String zipcode) {
CityRepository.executorService.execute(() -> {
address.postValue(repository.getAddress(zipcode));
});
}
}
サンプルコード
とここで、自身が書いたコードをGitHubに載っけているので、参考の一つにしてみてください。
おわりに
今回は、Retrofit2を使用した際に自身が躓いた箇所の原因と対処法を記しました。
- Call.enqueue()→非同期メソッド、Call.execute()→同期メソッド
- Call.execute()はメインスレッドでは実行できない
- 非同期処理は上から順に処理されない
この辺りを注意して実装していくのが良いのかなと感じました。この記事が参考になると光栄です。以上です。ありがとうございました!