OkHttpの非同期ラッパークラスの設計
はじめに
でokhttpの同期通信について言及しましたが、非同期利用がまあメインなわけで
やっぱりそのまま使おうとすると面倒なんですよね。。
new Handlerの処理を追記したりとか・・、色々とやることがある・・・
でそこら辺を Okttp3ベース で書き出してみる
参考にしたの
やはり同じことを考える人は多いようで、Githubには似たようなOkhttpラッパーが多いです
- AsyncOkHttpClient
- ApiService
とか。。まあ今回は勉強のため自作してみます
OkHttpUtil
Okttp3からは、自前でOkHttpClientを singleton にするように言われているので、それ用のクラスを作成する
一応3時間ほど通信キャッシュを残すみたいな記述にしてみましたが、httpsとかはキャッシュ無理かなーとか思います
import android.content.Context; import nihon_tc.com.ssltest.application.HogeApplication; import okhttp3.Cache; import okhttp3.OkHttpClient; import java.io.File; import java.util.concurrent.TimeUnit; public class OkHttpUtil { private static OkHttpClient client; public static OkHttpClient getOkhttpClient() { if(client != null){ return client; } OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS); //キャッシュコントロールとか long currentTimeMillis = System.currentTimeMillis(); currentTimeMillis -= 3 * 60 * 60 *1000;//3時間前のデータを消す Context context = HogeApplication.getInstance().getApplicationContext(); int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 MiB File cachedFile = new File(context.getCacheDir(), "responses"); if(cachedFile.exists() && cachedFile.lastModified() < currentTimeMillis){ cachedFile.delete(); } Cache cache = new Cache(cachedFile, MAX_CACHE_SIZE); builder.cache(cache); client = builder.build(); return client; } }
AsyncOkHttpClient
Qittaあたりのコードを参考に実装
import android.os.Handler; import android.os.Looper; import nihon_tc.com.ssltest.util.OkHttpUtil; import okhttp3.*; import java.io.IOException; public class AsyncOkHttpClient { private AsyncOkHttpClient() { } private static OkHttpClient getOkHttpClient(){ return OkHttpUtil.getOkhttpClient(); } public static Response get(HttpUrl httpUrl,Callback callback) { return get(httpUrl, null, callback); } public static Response get(HttpUrl httpUrl, Headers headers, Callback callback) { Request.Builder builder = new Request.Builder(); //[TODO]StackOverFlowに載ってた tag付け。tag経由でキャンセルできる? String pathSegments = ""; for (String s : httpUrl.encodedPathSegments()) { pathSegments += s; } builder.tag(pathSegments); builder.url(httpUrl.url()).get(); if(headers != null){ builder.headers(headers); } return execute(builder.build(), callback); } // tag経由で実行キャンセルする処理 public static void cancel(final String tag){ for(Call call : getOkHttpClient().dispatcher().queuedCalls()) { if(call.request().tag().equals(tag)) call.cancel(); } for(Call call : getOkHttpClient().dispatcher().runningCalls()) { if(call.request().tag().equals(tag)) call.cancel(); } } public static void post(HttpUrl httpUrl, Headers headers, RequestBody requestBody, final Callback callback){ Request.Builder builder = new Request.Builder(); builder.url(httpUrl.url()); builder.headers(headers); builder.post(requestBody); execute(builder.build(), callback); } private static Response execute(final Request request, final Callback callback) { getOkHttpClient().newCall(request).enqueue(new okhttp3.Callback() { final Handler mainHandler = new Handler(Looper.getMainLooper()); @Override public void onFailure(Call call, final IOException e) { mainHandler.post(new Runnable() { @Override public void run() { callback.onFailure(null, e); } }); } @Override public void onResponse(Call call, final Response response) throws IOException { final String content = response.body().string(); boolean isSuccessful = response.isSuccessful(); if (isSuccessful) { mainHandler.post(new Runnable() { @Override public void run() { if (callback != null) { callback.onSuccess(response, content); } } }); } else { mainHandler.post(new Runnable() { @Override public void run() { if (callback != null) { callback.onFailure(response, null); } } }); } } }); return null; } public interface Callback { public void onFailure(Response response, Throwable throwable); public void onSuccess(Response response, String content); } }
Retryしたい状況を考えてみる
OkHttp3で非同期だと、
public AsyncOkHttpClient{ public interface Callback { public void onFailure(Response response, Throwable throwable); public void onSuccess(Response response, String content); } }
みたいなコールバック二通りの実装(通信成功 と 通信失敗)になるかと思います。
Githubの OkHttpClientラッパーも同じ感じですね。
- でも下記の状況でも IOException が飛ぶわけです
IOExceptionの中で上記の条件かどうか切り分けるのは正直困難。
事前にチェックする方法を検討するのがよいかと
少なくとも上記の状態から復帰したときは、再度通信できないかなと思うのも自然かと。
あとは通信リトライを諦めた場合の後始末をするかとかもですね。。。
public AsyncOkHttpClient{ public interface Callback { public void onFailure(Response response, Throwable throwable); public void onSuccess(Response response, String content); public void onRetryCancel(); } }
とりあえず、圏外の場合のケースを考えてみます
// 通信環境の確認 public boolean checkNetWorkConnected(final OnAccessNetworkStateCallback onAccessNetworkStateCallback){ if(isNetWorkConnected(mContext)){ if(onAccessNetworkStateCallback != null){ onAccessNetworkStateCallback.onAccessNetworkState(); } return true; } if(onAccessNetworkStateCallback == null){ return false; } AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.Base_Theme_AppCompat_Light_Dialog_Alert) .setCancelable(false) .setTitle("確認") .setMessage("通信に失敗しました。再度通信しますか?") .setPositiveButton("通信する", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { checkNetWorkConnected(onAccessNetworkStateCallback);//再帰呼び出し } }) .setNegativeButton("キャンセル", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { if(onAccessNetworkStateCallback != null){ onAccessNetworkStateCallback.onCancelNetworkState(); } } }); builder.show(); return false; } //ネットワークが接続されているか public boolean isNetWorkConnected(Context context){ ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = cm.getActiveNetworkInfo(); if( info != null ){ return info.isConnected(); } return false; } // 通信環境がOKなときに呼ばれるイベントリスナー public static interface OnAccessNetworkStateCallback { void onAccessNetworkState(); void onCancelNetworkState(); }
RestService
AsyncOkHttpClient にNWチェック機構を入れるとごちゃごちゃしそうなので、更にくるんだクラスを設計する
ただまあ、RestService とか書くと retrofit と勘違いして怒る人いるかも。。(苦笑
public class RestService{ private Context mContext; public RestService(Context context) { mContext = context; } public Response get(final HttpUrl httpUrl, final Headers headers, final AsyncOkHttpClient.Callback callback){ if(mContext instanceof Activity) { if (((Activity)mContext).isFinishing()) { return null; } } //同期通信のコード if(callback == null){ if(checkNetWorkConnected(null)){ return AsyncOkHttpClient.get(httpUrl, headers, null); } return null; } //通信環境のチェック checkNetWorkConnected(new OnAccessNetworkStateCallback() { @Override public void onAccessNetworkState() { //通信開始 AsyncOkHttpClient.get(httpUrl, headers, callback); } @Override public void onCancelNetworkState() { callback.onRetryCancel(); } }); return null; } public void post(final HttpUrl httpUrl, final Headers headers, final RequestBody requestBody, final AsyncOkHttpClient.Callback callback){ if(mContext instanceof Activity) { if (((Activity)mContext).isFinishing()) { return; } } //通信環境のチェック checkNetWorkConnected(new OnAccessNetworkStateCallback() { @Override public void onAccessNetworkState() { //通信開始 AsyncOkHttpClient.post(httpUrl, headers, requestBody, callback); } @Override public void onCancelNetworkState() { callback.onRetryCancel(); } }); } public void post(final HttpUrl httpUrl, final Headers headers, final String json, final AsyncOkHttpClient.Callback callback){ if(mContext instanceof Activity) { if (((Activity)mContext).isFinishing()) { return; } } //通信環境のチェック checkNetWorkConnected(new OnAccessNetworkStateCallback() { @Override public void onAccessNetworkState() { //jsonがあればボディーを作成 RequestBody requestBody = null; if (!TextUtils.isEmpty(json)) { // リクエストボディの作成 try { final String s = new String(json.getBytes(), "UTF-8"); } catch (UnsupportedEncodingException e) { Log.e("utf8", "conversion", e); } requestBody = RequestBody.create(MediaType.parse("application/json"), json); } //通信開始 AsyncOkHttpClient.post(httpUrl, headers, requestBody, callback); } @Override public void onCancelNetworkState() { callback.onRetryCancel(); } }); } }
RestUtil
- 呼び出すurlの構築をして、RestServiceを呼び出すイメージで作成する
- RestService までを共通ライブラリとかに入れるとしたら、このクラスはアプリ側でガリガリ使う方のイメージ
import android.content.Context; import okhttp3.HttpUrl; public class RestUtil { //URLのSCHEMEの定数 public static String SCHEME_HTTPS = "https"; public static String SCHEME_HTTP = "http"; public static String BASE_SCHEME = "https"; public static String BASE_HOST = "www.hoge.com"; public static int BASE_PORT = 443; //カスタムURL用 public static HttpUrl.Builder createBaseUrl(){ HttpUrl.Builder builder = new HttpUrl.Builder() .scheme(BASE_SCHEME) .host(BASE_HOST) .port(BASE_PORT) .addPathSegment("api") .addPathSegment("v1") .addQueryParameter("sensor","false"); return builder; } public static void getApiA(Context context, String address, AsyncOkHttpClient.Callback callback){ HttpUrl.Builder builder = createBaseUrl(); builder.addQueryParameter("address","address"); new RestService(context).get(builder.build(), null, callback); } }