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

OkHttpの非同期ラッパークラスの設計

android okhttp

はじめに

でokhttpの同期通信について言及しましたが、非同期利用がまあメインなわけで

やっぱりそのまま使おうとすると面倒なんですよね。。

new Handlerの処理を追記したりとか・・、色々とやることがある・・・

でそこら辺を Okttp3ベース で書き出してみる

参考にしたの

やはり同じことを考える人は多いようで、Githubには似たようなOkhttpラッパーが多いです

  • AsyncOkHttpClient 
  • ApiService

とか。。まあ今回は勉強のため自作してみます

OkHttpUtil

Okttp3からは、自前でOkHttpClientを singleton にするように言われているので、それ用のクラスを作成する

一応3時間ほど通信キャッシュを残すみたいな記述にしてみましたが、httpsとかはキャッシュ無理かなーとか思います

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時間前のデータを消す 

        int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 MiB
        File cacheFile = new File(context.getCacheDir(), "responses");
        if(cacheFile.exists() && cacheFile. lastModified() < currentTimeMillis){
              cachedFile.delete();
        }

        Cache cache = new Cache(cacheFile, MAX_CACHE_SIZE);
        builder.setCache(cache);

        client  = builder.build();
        return client;
    }
}

AsyncOkHttpClient

Qittaあたりのコードを参考に実装

public class AsyncOkHttpClient {

    private AsyncOkHttpClient() {
    }

    private static OkHttpClient getOkhttpClient(){
          return OkHttpUtil.getOkhttpClient();
    }

    public static void 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 void execute(final Request request, final Callback callback) {
        getOkhttpClient().newCall(request).enqueue(new com.squareup.okhttp.Callback() {
            final Handler mainHandler = new Handler(Looper.getMainLooper());

            @Override
            public void onFailure(final Request request, final IOException e) {
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onFailure(null, e);
                    }
                });
            }

            @Override
            public void onResponse(final Response response) throws IOException {
                if (response.isSuccessful()) {
                    final String content = response.body().string();
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onSuccess(response, content);
                        }
                    });
                } else {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onFailure(response, 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 OnNetworkCallback onNetworkCallback){
        if(isNetWorkConnected(mContext)){
            if(onNetworkCallback != null){
                onNetworkCallback.onAccessNetwork();
            }
            return true;
        }else{
            if(onNetworkCallback != null){
                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(onNetworkCallback);//再帰呼び出し
                            }
                        })
                        .setNegativeButton("キャンセル", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                if(onNetworkCallback != null){
                                    onNetworkCallback.onCancelNetwork();
                                }
                            }
                        });
                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();
        } else {
            return false;
        }
    }


    public interface OnNetworkCallback {
        // 通信環境がOKのとき
        void onAccessNetwork();

        //通信のリトライを諦めた時
        void onCancelNetwork();
    }

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 OnNetworkCallback() {
            @Override
            public void onAccessNetwork() {
                //通信開始
                AsyncOkHttpClient.get(httpUrl, headers, callback);
            }

            @Override
            public void onCancelNetwork() {
                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 OnNetworkCallback() {
            @Override
            public void onAccessNetwork() {
                //通信開始
                AsyncOkHttpClient.post(httpUrl, headers, requestBody, callback);
            }

            @Override
            public void onCancelNetwork() {
                callback.onRetryCancel();
            }
        });
    }

    public void post(final HttpUrl httpUrl, final Headers headers, final String json, final AsyncOkHttpClient.Callback callback){
        RequestBody requestBody = null;
        if (!TextUtils.isEmpty(json)) {
            try {
                final String s = new String(json.getBytes(), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                Log.e("utf8", "post", e);
            }
            requestBody = RequestBody.create(MediaType.parse("application/json"), json);
        }
        post(httpUrl,headers, final Headers requestBody,callback);
    }
}

RestUtil

  • 呼び出すurlの構築をして、RestServiceを呼び出すイメージで作成する
  • RestService までを共通ライブラリとかに入れるとしたら、このクラスはアプリ側でガリガリ使う方のイメージ
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(String address, AsyncOkHttpClient.Callback callback){
        HttpUrl.Builder builder = createBaseUrl();
        builder.addQueryParameter("address","address");

        RestService(context).get(builder.build(), null, callback);
    }
    
}