Androidの通信ライブラリの歴史を振り返る

  • 77
    いいね
  • 0
    コメント

はじめに

なぜ、今更こんな何のためにもならなそうなことを書くのかというお話をします。
最近Androidを新しく始める人はすいすいとアプリの開発を覚えていき、昔は大変だった通信もRetrofit一択で何の迷いもなく終わります。Retrofitは本当に素晴らしいライブラリです、アノテーションを使用してコードを殆ど書かず、初心者にも分かりやすく書くことができ、カスタマイズ性も非常に高いです。
ですが、だからこそ昔のAndroidの通信の長い歴史を知って、今まで以上にRetrofitなどのライブラリの素晴らしさを実感してほしいと思って書きました。
そして、できれば誰かが次世代の通信ライブラリを作る際の糧としてくれることを願っています。

主な歴史

Androidでの通信といえば、HttpClientでした。HttpClientといっても、DefaultHttpClientと呼ばれるApache HTTP Clientや、AndroidHttpClientというHttp Clientの亜種などが使用されていました。
ですが、HttpClientにはいくつかのバグがあるということで、2011年にはGoogleのAndroid DevelopersのブログでHttpUrlConnectionが推奨されてからは、こちらが主に使用されていたようです。

その後、Volleyは使い方が複雑だったHttpUrlConnectionの代わりとして、Google製だということもあり、スタンダードなライブラリとして使用されました。Square製のOkHttpも人気が高く、多く使用されていました。

ですが、Android5.1でHttpClientがDeprecatedになってから、HTTPClientに依存しているVolleyもDeprecatedになりました。Square製のライブラリであるOkHttpと、そのラッパーであるRetrofitの一択といった状態になりました。Retofitでは、通信のクライアント部分や、コールバックの形式などをプラガブルに変更することが可能なため非常に人気です。

実装を比較する

改めて実装を比べてみたいと思います。
また、まだ流行るかはわかりませんが、ionというライブラリも載せておきます。

今回、記事として載せるのはこちらです。

  • DefaultHttpClient
  • HttpUrlConnection
  • Volley
  • OkHttp
  • Retrofit2
  • ion

プロジェクトについて

今回は、シンプルに天気予報のAPIをGETリクエストするだけのサンプルを作成しました。
POSTの部分で変わる部分も多くあると思いますが、ライブラリの使い勝手はわかると思うので一旦無視しました。

使用するAPIは、Livedoorの天気予報APIです。
http://weather.livedoor.com/

プロジェクトの構成について

プロジェクトの構成は、ライブラリの使用方法が分かりやすいように、DIなどでライブラリは使用せずに、Javaの言語機能のみ使用して作成しました。
アプリとして行うことは、各ライブラリごとに通信を行い、その結果をGsonを使用して各Objectにパースし、それを表示するところまでです。通信に使用するライブラリは、Spinnerを使用して変更できるようにしています。

パッケージの構成は、以下の通りです。

  • repository
    • WeatherRepository : Repositoryのインターフェース
    • WeatherRepositoryImpl{LibraryName} : ライブラリごとの実装クラス
  • entities

通信を行うRepositoryのインターフェースは以下になります。
Rxを使うとスレッドをいじれるため便利なのですが、ライブラリの使い方の使い勝手がわかりにくくなってしまうので、以下のような形でコールバックを揃えました。
getWeatherがライブラリが実装するべき部分となります。

WeatherRepository.java
public interface WeatherRepository {
    // URL Const Vals
    String SCHEME = "http";
    String AUTHORITY = "weather.livedoor.com";
    String PATH = "/forecast/webservice/json/v1";
    // URI
    Uri uri = new Uri.Builder()
            .scheme(SCHEME)
            .authority(AUTHORITY)
            .path(PATH)
            .appendQueryParameter("city", "130010")
            .build();

    void getWeather(RequestCallback callback);

    interface RequestCallback {
        // 成功時のCallback
        void success(Weather weather);

        // 失敗次のCallback
        void error(Throwable throwable);
    }

}

また、各Repositoryのインスタンス管理は、RepositoryProviderが行います。

各ライブラリの実装について

DefaultHttpClient

DefaultHttpClientを使用した例です。
Android6でDefaultHttpClientは消されてしまっているため、こちらを使用するにはbuild.gradleuseLibraryを加えます。

app/build.gradle
android {
    useLibrary 'org.apache.http.legacy'
}

HttpClientは、HttpClientインスタンスを作成し、GetリクエストであればHttpGet、PostリクエストであればHttpPostを使用します。Responseは、HttpResponse形式で返ってきます。

辛いポイントとして、非同期処理なども自分で処理しないと行けないので、AsyncTaskで包んであげる必要があります。

WeatherRepositoryImplHttpClient.java
public class WeatherRepositoryImplHttpClient implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplHttpClient.class.getSimpleName();

    @Override
    public void getWeather(final RequestCallback callback) {
        new AsyncTask<Void, Void, Weather>() {
            // 処理の前に呼ばれるメソッド
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
            }

            // 処理を行うメソッド
            @Override
            protected Weather doInBackground(Void... params) {
                final HttpClient httpClient = new DefaultHttpClient();
                final HttpGet httpGet = new HttpGet(uri.toString());
                final HttpResponse httpResponse;
                try {
                    httpResponse = httpClient.execute(httpGet);
                    final String response = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");
                    return new Gson().fromJson(response, Weather.class);
                } catch (IOException e) {
                    return null;
                }
            }

            // 処理がすべて終わったら呼ばれるメソッド
            @Override
            protected void onPostExecute(Weather response) {
                super.onPostExecute(response);
                // 通信失敗として処理
                if (response == null) {
                    callback.error(new IOException("HttpClient request error"));
                } else {
                    Log.d(TAG, "result: " + response.toString());
                    // 通信結果を表示
                    callback.success(response);
                }
            }
        }.execute();
    }
}

HttpURLConnection

HttpURLConnectionを使用した例です。
HttpURLConnectionは、URLを生成し、そこにコネクションを張ります。
レスポンスは、バッファー形式で受け取るため自分でStringに変換します。

こちらも同様に、非同期処理にしなければ行けないのでつらいですね。

WeatherRepositoryImplHttpURLConnection.java
public class WeatherRepositoryImplHttpURLConnection implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplHttpURLConnection.class.getSimpleName();

    @Override
    public void getWeather(final RequestCallback callback) {
        new AsyncTask<Void, Void, Weather>() {
            // 処理の前に呼ばれるメソッド
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
            }

            // 処理を行うメソッド
            @Override
            protected Weather doInBackground(Void... params) {
                final HttpURLConnection urlConnection;
                try {
                    URL url = new URL(uri.toString());
                    urlConnection = (HttpURLConnection) url.openConnection();
                    urlConnection.setRequestMethod("GET");
                    urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
                } catch (MalformedURLException e) {
                    return null;
                } catch (IOException e) {
                    return null;
                }
                final String buffer;
                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));
                    buffer = reader.readLine();
                } catch (IOException e) {
                    return null;
                } finally {
                    urlConnection.disconnect();
                }
                if (TextUtils.isEmpty(buffer)) {
                    return null;
                }
                return new Gson().fromJson(buffer, Weather.class);
            }

            // 処理がすべて終わったら呼ばれるメソッド
            @Override
            protected void onPostExecute(Weather response) {
                super.onPostExecute(response);
                // 通信失敗として処理
                if (response == null) {
                    callback.error(new IOException("HttpURLConnection request error"));
                } else {
                    Log.d(TAG, "result: " + response.toString());
                    // 通信結果を表示
                    callback.success(response);
                }
            }
        }.execute();
    }
}

Volley

Volleyを使用した例です。
Volleyでは、非同期処理をラップしてくれているのでAsyncTaskなど使用しなくていいです。
今回はJsonを取得したいので、JsonObjectRequestを行います。Header情報なども勝手に付けてくれます。

WeatherRepositoryImplVolley.java
public class WeatherRepositoryImplVolley implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplVolley.class.getSimpleName();

    RequestQueue queue;

    public WeatherRepositoryImplVolley(Context context) {
        queue = Volley.newRequestQueue(context);
    }

    @Override
    public void getWeather(final RequestCallback callback) {
        final JsonObjectRequest request =
                new JsonObjectRequest(uri.toString(), null, new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        Log.d(TAG, "result: " + response.toString());
                        final Weather weather = new Gson().fromJson(response.toString(), Weather.class);
                        callback.success(weather);
                    }
                }, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        callback.error(error);
                    }
                });
        queue.add(request);

    }
}

OkHttp

OkHttpを使用した例です。
OkHttpは、通信を同期処理として行うか、非同期処理として行うか選ぶことができます。
ただし、スレッドをまたげないのでHandlerを使用します。

WeatherRepositoryImplOkHttp3.java
public class WeatherRepositoryImplOkHttp3 implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplOkHttp3.class.getSimpleName();

    private Handler handler = new Handler();

    @Override
    public void getWeather(final RequestCallback callback) {
        final Request request = new Request.Builder()
                // URLを生成
                .url(uri.toString())
                .get()
                .build();
        // クライアントオブジェクトを作成する
        final OkHttpClient client = new OkHttpClient();
        // 新しいリクエストを行う
        client.newCall(request).enqueue(new Callback() {
            // 通信が成功した時
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 通信結果をログに出力する
                final String responseBody = response.body().string();
                Log.d(TAG, "result: " + responseBody);
                final Weather weather = new Gson().fromJson(responseBody, Weather.class);
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.success(weather);
                    }
                });
            }

            // 通信が失敗した時
            @Override
            public void onFailure(Call call, final IOException e) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.error(e);
                    }
                });
            }
        });
    }
}

Retrofit

Retrofitを使用した例です。
Retrofitは、アノテーションによるコードを生成を行うため、そのためのインターフェースを作成します。

WeatherRepositoryImplRetrofit2.java
public class WeatherRepositoryImplRetrofit2 implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplRetrofit2.class.getSimpleName();

    private final WeatherService service;

    public WeatherRepositoryImplRetrofit2() {
        final Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(new Uri.Builder().scheme(SCHEME).authority(AUTHORITY).build().toString())
                .client(new OkHttpClient())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        service = retrofit.create(WeatherService.class);
    }

    @Override
    public void getWeather(final RequestCallback callback) {
        service.getWeather(130010).enqueue(new Callback<Weather>() {
            @Override
            public void onResponse(Call<Weather> call, Response<Weather> response) {
                Log.d(TAG, "result: " + response.body().toString());
                callback.success(response.body());
            }

            @Override
            public void onFailure(Call<Weather> call, Throwable error) {
                callback.error(error);
            }
        });
    }

    private interface WeatherService {
        @GET(PATH)
        Call<Weather> getWeather(@Query("city") int city);
    }

}

ion

ionを使用した例です。
ionは非常にシンプルなライブラリですが、スレッドなどもいい感じにやってくれます。
callbackの形は、asという部分でコントロールすることが出来、TypeTokenを指定すると裏でGsonを使ってオブジェクトのパースを行います。

WeatherRepositoryImplIon.java
public class WeatherRepositoryImplIon implements WeatherRepository {
    public static final String TAG = WeatherRepositoryImplIon.class.getSimpleName();

    private Context context;

    public WeatherRepositoryImplIon(Context context) {
        this.context = context;
    }

    @Override
    public void getWeather(final RequestCallback callback) {
        // クライアントオブジェクトを作成する
        // 新しいリクエストを行う
        Ion.with(context)
                .load(uri.toString())
                .as(new TypeToken<Weather>() {
                })
                .setCallback(new FutureCallback<Weather>() {
                    // 通信が終了した時
                    @Override
                    public void onCompleted(Exception e, Weather weather) {
                        // 通信が失敗した時
                        if (e != null) {
                            callback.error(e);
                            return;
                        }
                        // 通信結果をログに出力する
                        Log.d(TAG, "result: " + weather.toString());
                        callback.success(weather);
                    }
                });
    }
}

最後に

やはり、Retrofitは素晴らしいですね。
ですが、今回使用してみてionもシンプルに使用できるため教育目的などでは非常にいいんじゃないかと思いました。

今回のソースはこちらになります。
他にも、Retrofit1やOkHttp2などの使用バージョンもこちらに載せてあります。
教育的にも面白いと思うので見てみてもらえればと思います。

https://github.com/Reyurnible/AndroidNetworkHistory

コメントに、みなさんの黒歴史を投稿していっていただければ嬉しいです。お待ちしております。

参考

この投稿は Android Advent Calendar 20167日目の記事です。