読者です 読者をやめる 読者になる 読者になる

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);
    }
}