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.gradle
の dependencies
セクションに
compile 'com.squareup.okhttp:okhttp:2.0.0'
を追加して、 Tools → Android → Sync Project with Gradle Files でインストールされます。
単体で使う
OkHttp を使うときの流れはシンプルです。
-
Request
オブジェクトを作る -
OkHttpClient
インスタンスのnewCall
メソッドに渡してスタンバイする -
OkHttpClient
インスタンスのexecute
メソッドを実行してResponse
オブジェクトを受け取る - 結果を使う
この流れです。この他、 Header
オブジェクトや POST で通信するときなどに RequestBody
オブジェクトを作り、随時 Request
オブジェクトに渡してあげることでリクエストが完成します。
GET リクエスト
例えば R.id.button
で参照できるボタンを押したら結果を取ってくることを想定します。コード例は Butter Knife を使っています。
@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
で括っていますが、これ以外にも
// リクエストして結果を受け取って表示する
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
メソッドを使うことです。
Request request = new Request.Builder()
.addHeader("Content-Length", String.valueOf(requestBody.contentLength()))
.url("http://httpbin.org/post")
.post(requestBody)
.build();
一件うまくいきそうですが、これはうまくいきません。実は contentLength
メソッドは常に -1 を返してきます。ではどうするのかというと、一度バッファに書き出して、そこから長さを取得します。
// 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 コード互換の設定を書いてやる必要もあります。
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? を見ると設定できますので方法を紹介。
- okhttp-urlconnection をダウンロードする
- ダウンロードした jar ファイルを設置する
-
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 に良い感じのレシピがまとまっています。キャッシュコントロールやリクエストキャンセルなどを行う方法も載っていますので、本格的に使う場合の情報源として良さそうです。