はじめに
なぜ、今更こんな何のためにもならなそうなことを書くのかというお話をします。
最近Androidを新しく始める人はすいすいとアプリの開発を覚えていき、昔は大変だった通信もRetrofit一択で何の迷いもなく終わります。Retrofitは本当に素晴らしいライブラリです、アノテーションを使用してコードを殆ど書かず、初心者にも分かりやすく書くことができ、カスタマイズ性も非常に高いです。
ですが、だからこそ昔のAndroidの通信の長い歴史を知って、今まで以上にRetrofitなどのライブラリの素晴らしさを実感してほしいと思って書きました。
そして、できれば誰かが次世代の通信ライブラリを作る際の糧としてくれることを願っています。
主な歴史
- 2007/11/05 : Androidが発表される
- 2011/09/29 : HttpURLConnectionを推奨するブログが出る
- 2013/05/06 : OkHttpの1.0.0がリリースされる
- 2013/05/14 : Retrofitの1.0.0がリリースされる
- 2013/05/21 : Volleyがリリースされる
- 2016 : Android6.0でHttpClientが削除される
- 2016/03/12 : Retrofit2がリリースされる
- 2018/11 : Ktor1.0がリリースされる
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
がライブラリが実装するべき部分となります。
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.gradle
でuseLibrary
を加えます。
android {
useLibrary 'org.apache.http.legacy'
}
HttpClientは、HttpClientインスタンスを作成し、GetリクエストであればHttpGet、PostリクエストであればHttpPostを使用します。Responseは、HttpResponse形式で返ってきます。
辛いポイントとして、非同期処理なども自分で処理しないと行けないので、AsyncTaskで包んであげる必要があります。
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に変換します。
こちらも同様に、非同期処理にしなければ行けないのでつらいですね。
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情報なども勝手に付けてくれます。
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を使用します。
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は、アノテーションによるコードを生成を行うため、そのためのインターフェースを作成します。
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を使ってオブジェクトのパースを行います。
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);
}
});
}
}
Ktor Client
Ktorは、Kotlin性の軽量なWebフレームワークとして有名ですが、その中にあるKtor Clientを使うとMulti Platformなどへの対応が容易ということで人気があります。
今回は、もとがJavaのコードだったので全体を書き換えたりせずここだけKotlinで書いて、ただスレッド周りの処理とかはCoroutineを使うケースがほとんどだと思うのでCoroutineでシンプルに書いたものを上げておきます。
/**
* KtorClientでの実装
* https://ktor.io/clients/http-client.html
*/
class WeatherRepositoryImplKtorClient : WeatherRepository {
private val client = HttpClient(Android) {
install(JsonFeature) {
serializer = GsonSerializer()
}
engine {
connectTimeout = (10 * DateUtils.SECOND_IN_MILLIS).toInt()
socketTimeout = (10 * DateUtils.SECOND_IN_MILLIS).toInt()
}
}
override fun getWeather(callback: WeatherRepository.RequestCallback) {
try {
GlobalScope.launch(Dispatchers.Main) {
val response = requestGet<Weather>(url = uri.toString()).await()
callback.success(response)
}
} catch (cause: Throwable) {
callback.error(cause)
}
}
private suspend inline fun <reified T> requestGet(url: String, params: Map<String, Any> = mapOf()): Deferred<T> {
val request = GlobalScope.async {
val response = client.get<T>(url)
client.close()
return@async response
}
return request
}
companion object {
val TAG = WeatherRepositoryImplKtorClient::class.java.simpleName
}
}
久しぶりにいじったGitHubのソースコード側のPRを見てもらうのが分かりやすいかもしれません。
該当のPR
最後に
やはり、Retrofitは素晴らしいですね。
ですが、今回使用してみてionもシンプルに使用できるため教育目的などでは非常にいいんじゃないかと思いました。
今回のソースはこちらになります。
他にも、Retrofit1やOkHttp2などの使用バージョンもこちらに載せてあります。
教育的にも面白いと思うので見てみてもらえればと思います。
https://github.com/Reyurnible/AndroidNetworkHistory
コメントに、みなさんの黒歴史を投稿していっていただければ嬉しいです。お待ちしております。