OkHttp 2.0 を使ってみた

  • 248
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

Android アプリで HTTP 通信を行うときは Volley を使っているのですが、とある事情により、もうちょっと直接的 (?) に HTTP 通信を行う必要がある場面に出くわしたときに使ってみたのが Square の OkHttp 2.0 です。

An HTTP & SPDY client for Android and Java applications

とあるように、 SPDY にも対応していて、 Android に限らず、 Java アプリケーション中でも使えるようです。今回は Android プロジェクトで使ってみた雑感を書いてみたいと思います。

プロジェクトへのインストール

OkHttp のサイトには jar ファイルをダウンロードするか、 MAVEN の設定をする方法が書いてありますが、 Android Studio ならば build.gradledependencies セクションに

compile 'com.squareup.okhttp:okhttp:2.0.0'

を追加して、 Tools → Android → Sync Project with Gradle Files でインストールされます。

単体で使う

OkHttp を使うときの流れはシンプルです。

  1. Request オブジェクトを作る
  2. OkHttpClient インスタンスの newCall メソッドに渡してスタンバイする
  3. OkHttpClient インスタンスの execute メソッドを実行して Response オブジェクトを受け取る
  4. 結果を使う

この流れです。この他、 Header オブジェクトや POST で通信するときなどに RequestBody オブジェクトを作り、随時 Request オブジェクトに渡してあげることでリクエストが完成します。

GET リクエスト

例えば R.id.button で参照できるボタンを押したら結果を取ってくることを想定します。コード例は Butter Knife を使っています。

AsyncTaskで非同期化
@OnClick(R.id.button)
public void getRequest(final Button button) {
    button.setEnabled(false);

    new AsyncTask<Void, Void, String>() {
        @Override
        protected String doInBackground(Void... params) {
            String result = null;

            // リクエストオブジェクトを作って
            Request request = new Request.Builder()
                    .url("http://httpbin.org/headers")
                    .get()
                    .build();

            // クライアントオブジェクトを作って
            OkHttpClient client = new OkHttpClient();

            // リクエストして結果を受け取って
            try {
                Response response = client.newCall(request).execute();
                result = response.body().string();
            } catch (IOException e) {
                e.printStackTrace();
            }

            // 返す
            return result;
        }

        @Override
        protected void onPostExecute(String result) {
            Log.d(TAG, result);
            button.setEnabled(true);
        }
    }.execute();
}
結果
08-09 14:46:03.352    1737-1737/net.imthinker.android.performmap D/MyActivity﹕ {
    "headers": {
    "Accept-Encoding": "gzip",
    "Connection": "close",
    "Host": "httpbin.org",
    "X-Request-Id": "621ba1b2-8d44-4f36-a483-a3625b44fcc1"
    }
    }

AsyncTask で括っているのは、そのまま実行すると android.os.NetworkOnMainThreadException が発生するからです (Android 3.0 以降の Strict Mode によるもの) 。このことから、 OkHttp はただ素直に使うとメインスレッドで実行しようとすることが判ります。明示的に別のスレッドで非同期処理しますよーという、コードを書いてあげる必要があるということですね。 OkHttp のサイトにある Examples はサラリと実行部のコードだけが書いてあるので、見逃しがちです。

今回は AsyncTask で括っていますが、これ以外にも

enqueueメソッドを使った非同期化
// リクエストして結果を受け取って表示する
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Request request, IOException e) {
        e.printStackTrace();
        // 頂いたコメントにあるとおり、 Callback はワーカースレッドで動作するので UI を操作できない
        // button.setEnabled(true);
    }

    @Override
    public void onResponse(Response response) throws IOException {
        String result = response.body().string();
        Log.d(TAG, result);
        // 頂いたコメントにあるとおり、 Callback はワーカースレッドで動作するので UI を操作できない
        // button.setEnabled(true);
    }
});

execute メソッドを使わずに enqueue メソッドを使うことでインスタントに非同期処理を行うこともできます。普段はこちらでも十分に使える気がしますね。

POST リクエスト

POST もほとんど GET と変わりません。ただし、リクエストボディに色々と渡すことができます。

// リクエストボディを作る
RequestBody requestBody = RequestBody.create(
        MediaType.parse("text/plain"), "Hello world!"
);

// リクエストオブジェクトを作って
Request request = new Request.Builder()
        .url("http://httpbin.org/post")
        .post(requestBody)
        .build();

Request オブジェクトを作るときのビルダーで post メソッドを呼び出して、中には RequestBody オブジェクトを入れました。 RequestBody はスタティックメソッドの create で作ることができます。この create メソッドが便利なやつで、 java.io.File オブジェクトを渡すこともできます。直感的ですね。

マルチパート POST リクエスト

マルチパートの POST を行いたいときも OkHttp は簡単にできました。

マルチパート
// リクエストボディを作る
final MediaType TEXT = MediaType.parse("text/plain; charset=utf-8");
final String BOUNDARY = String.valueOf(System.currentTimeMillis());

RequestBody requestBody = new MultipartBuilder(BOUNDARY)
        .type(MultipartBuilder.FORM)
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"name\""),
                RequestBody.create(TEXT, "Android")
        )
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"version\""),
                RequestBody.create(TEXT, "4.4")
        )
        .addPart(
                Headers.of("Content-Disposition", "form-data; name=\"codename\""),
                RequestBody.create(TEXT, "KitKat")
        )
        .build();

Request request = new Request.Builder()
        .url("http://httpbin.org/post")
        .post(requestBody)
        .build();

HTTP を良い感じに抽象化していて、直感的に使うことができます。使っていて1つだけおこった問題があります。それは、リクエストボディの長さ (Content-Length) を取りたい場合です。 HTTP ヘッダに Content-Length を入れたい場合にまず思いつくのは、 RequestBody オブジェクトの contentLength メソッドを使うことです。

contentLengthメソッドの罠
Request request = new Request.Builder()
        .addHeader("Content-Length", String.valueOf(requestBody.contentLength()))
        .url("http://httpbin.org/post")
        .post(requestBody)
        .build();

一件うまくいきそうですが、これはうまくいきません。実は contentLength メソッドは常に -1 を返してきます。ではどうするのかというと、一度バッファに書き出して、そこから長さを取得します。

Bufferを使ってContent-Lengthを取得する
// okio.Buffer オブジェクト, OkHttp の中に入っている
Buffer buffer = new Buffer();
String CONTENT_LENGTH;
try {
    requestBody.writeTo(buffer);
    CONTENT_LENGTH = String.valueOf(buffer.size());
} catch (IOException e) {
    e.printStackTrace();
    CONTENT_LENGTH = "-1";
} finally {
    buffer.close();
}

Request request = new Request.Builder()
        .addHeader("Content-Length", CONTENT_LENGTH)
        .url("http://httpbin.org/post")
        .post(requestBody)
        .build();

実はこれにも罠があります …… というのは、マルチパートの POST リクエストを行うと、 OkHttp 2.0 では Content-Length ヘッダが消えてしまうのです。これでは、 POST 先によっては 411 Length Required が返ってきてしまいます。

この問題は既に認識されていて、 OkHttp の Pull Request を見ると Update MultipartBuilder to support content length. #969 にリクエストが送られています。2014年8月10日現在、まだマージされていません 2014年8月28日に確認したところ master ブランチにマージされてはいますが、まだ対応バージョンがリリースされていませんので、変更されている MultipartBuilder.java を自分のプロジェクト中に設置して使ってやると、ちゃんと Content-Length を設定することができました。

また、この MultipartBuilder.java は Java 1.7 の形になっているので、 build.gradle の中に 1.7 コード互換の設定を書いてやる必要もあります。

Java1.7設定
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
}
08-09 16:26:35.876    2380-2380/net.imthinker.android.performmap D/MyActivity﹕ {
    "args": {},
    "data": "",
    "files": {},
    "form": {
    "codename": "KitKat",
    "name": "Android",
    "version": "4.4"
    },
    "headers": {
    "Accept-Encoding": "gzip",
    "Connection": "close",
    "Content-Length": "418",
    "Content-Type": "multipart/form-data; boundary=1407601595433",
    "Host": "httpbin.org",
    "X-Request-Id": "d97c3a3b-fbcb-482b-a56f-c39f4a6014e5"
    },
    "json": null,
    "origin": "123.456.789.0",
    "url": "http://httpbin.org/post"
    }

うまくリクエストできてます。良いですね。

Volley で使う

普段は Volley で HTTP 通信をしているので、部分的に OkHttp を使うならば、 Volley の中でも OkHttp を使いたくなってきます。 OkHttp 2.0 になってからちょっとだけ設定が面倒になったみたいです。 Stack Overflow の How to implement Android Volley with OkHttp 2.0? を見ると設定できますので方法を紹介。

  1. okhttp-urlconnection をダウンロードする
  2. ダウンロードした jar ファイルを設置する
  3. OkHttpStack.java ファイルを作る
public class OkHttpStack extends HurlStack {
    private final OkUrlFactory okUrlFactory;

    public OkHttpStack() {
        this(new OkUrlFactory(new OkHttpClient()));
    }

    public OkHttpStack(OkUrlFactory okUrlFactory) {
        if (okUrlFactory == null) {
            throw new NullPointerException("Client must not be null.");
        }
        this.okUrlFactory = okUrlFactory;
    }

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        return okUrlFactory.open(url);
    }
}

あとは Volley の Volley.newRequestQueue に渡してあげるだけです。

RequestQueue queue = Volley.newRequestQueue(getApplicationContext, new OkHttpStack());

実際には Volley を Android アプリで使うときのオレオレプラクティス で紹介したように、 RequestQueue はシングルトン的に扱います。 Volley の HttpStack に OkHttp を設定するメリットがいかほどなのかは何も計測していないですが、精神的に良い感じになった気がしますねw

最後に

Volley はメチャクチャ高機能で便利ですが、 OkHttp は HTTP を使いやすい形で抽象化していて、サッと使える感じが良い感じだと思いました。書いているときに気がついたのですが、 GitHub の Wiki に良い感じのレシピがまとまっています。キャッシュコントロールやリクエストキャンセルなどを行う方法も載っていますので、本格的に使う場合の情報源として良さそうです。