OkHttp 2.0 を使ってみた

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

> An HTTP & SPDY client for Android and Java applications

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

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

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

```groovy
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](http://jakewharton.github.io/butterknife/) を使っています。

```java: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();
}
```

```text:結果
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` で括っていますが、これ以外にも

```java: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 と変わりません。ただし、リクエストボディに色々と渡すことができます。

```java
// リクエストボディを作る
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 は簡単にできました。

```java:マルチパート
// リクエストボディを作る
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` メソッドを使うことです。

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

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

```java: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](https://github.com/square/okhttp/pull/969) にリクエストが送られています。~~2014年8月10日現在、まだマージされていません~~ 2014年8月28日に確認したところ master ブランチにマージされてはいますが、まだ対応バージョンがリリースされていませんので、変更されている [MultipartBuilder.java](https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/main/java/com/squareup/okhttp/MultipartBuilder.java) を自分のプロジェクト中に設置して使ってやると、ちゃんと Content-Length を設定することができました。

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

```groovy:Java1.7設定
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
}
```

```text
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?](http://stackoverflow.com/questions/24375043/how-to-implement-android-volley-with-okhttp-2-0) を見ると設定できますので方法を紹介。

1. [okhttp-urlconnection](http://search.maven.org/remotecontent?filepath=com/squareup/okhttp/okhttp-urlconnection/2.0.0/okhttp-urlconnection-2.0.0.jar) をダウンロードする
2. ダウンロードした jar ファイルを設置する
3. `OkHttpStack.java` ファイルを作る

```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` に渡してあげるだけです。

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

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

## 最後に

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