「Androidアプリ開発の教科書」の11章「非同期処理とWeb API連携」のサンプルコードを
Web API連携でよく使われているライブラリ(Retrofit2 + Gson + Okhttp)でリファクタリングしてみました。
- Gson・・・JavaオブジェクトとJSONデータをシリアライズ/デシリアライズするライブラリ
- okhttp・・・HTTPおよびHTTP2のネットワーククライアントのライブラリ
- Retrofit2・・・ネットワーク通信のためのライブラリ
本書のサンプルはリストにある地域をタッチするとlivedoor天気情報のWebAPIを使って、
天気情報を表示するアプリです。
学習目的でサンプルを改造した過程を記事にします。
リファクタリング対象のコードはこちら↓
実行できる環境が欲しい方はこちらからダウンロードできます。
private inner class WeatherInfoReceiver(): AsyncTask<String, String, String>() {
override fun doInBackground(vararg params: String): String {
//可変長引数の1個目(インデックス0)を取得。これが都市ID
val id = params[0]
//都市IDを使って接続URL文字列を作成。
val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"
//URLオブジェクトを生成。
val url = URL(urlStr)
//URLオブジェクトからHttpURLConnectionオブジェクトを取得。
val con = url.openConnection() as HttpURLConnection
//http接続メソッドを設定。
con.requestMethod = "GET"
//接続。
con.connect()
//HttpURLConnectionオブジェクトからレスポンスデータを取得。天気情報が格納されている。
val stream = con.inputStream
//レスポンスデータであるInputStreamオブジェクトを文字列(JSON文字列)に変換。
val result = is2String(stream)
//HttpURLConnectionオブジェクトを解放。
con.disconnect()
//InputStreamオブジェクトを解放。
stream.close()
//JSON文字列を返す。
return result
}
override fun onPostExecute(result: String) {
//JSON文字列からJSONObjectオブジェクトを生成。これをルートJSONオブジェクトとする。
val rootJSON = JSONObject(result)
//ルートJSON直下の「description」JSONオブジェクトを取得。
val descriptionJSON = rootJSON.getJSONObject("description")
//「description」プロパティ直下の「text」文字列(天気概況文)を取得。
val desc = descriptionJSON.getString("text")
//ルートJSON直下の「forecasts」JSON配列を取得。
val forecasts = rootJSON.getJSONArray("forecasts")
//「forecasts」JSON配列のひとつ目(インデックス0)のJSONオブジェクトを取得。
val forecastNow = forecasts.getJSONObject(0)
//「forecasts」ひとつ目のJSONオブジェクトから「telop」文字列(天気)を取得。
val telop = forecastNow.getString("telop")
//天気情報用文字列をTextViewにセット。
val tvWeatherTelop = findViewById<TextView>(R.id.tvWeatherTelop)
val tvWeatherDesc = findViewById<TextView>(R.id.tvWeatherDesc)
tvWeatherTelop.text = telop
tvWeatherDesc.text = desc
}
/**
* InputStreamオブジェクトを文字列に変換するメソッド。変換文字コードはUTF-8。
*
* @param stream 変換対象のInputStreamオブジェクト。
* @return 変換された文字列。
*/
private fun is2String(stream: InputStream): String {
val sb = StringBuilder()
val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))
var line = reader.readLine()
while(line != null) {
sb.append(line)
line = reader.readLine()
}
reader.close()
return sb.toString()
}
}
手順1. gradleの追記
まず、既存のプロジェクトがRetrofit2、Gson、Okhttpのライブラリが使えるようにします
dependencies {
:
:
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation "com.squareup.retrofit2:retrofit:"
}
手順2. JSONコードから値を取得するコードをリファクタリング
livedoor天気情報のAPIは以下のJSONコードを返します。
{
"publicTime" : "2013-01-29T11:00:00+0900",
"title" : "福岡県 久留米 の天気",
"description" : {
"text" : " 九州北部地方は、高気圧に覆われて晴れています。\n\n 29日は、九州北部地方では、高気圧に覆われて晴れますが、気圧の谷の\n影響で、昼過ぎから次第に曇りとなるでしょう。\n\n 30日は、気圧の谷の影響ではじめ曇りますが、昼頃からは高気圧に覆わ\nれて概ね晴れるでしょう。\n\n 波の高さは、九州北部地方の沿岸の海域では、29日は1.5メートル、\n30日は1メートルでしょう。豊後水道では、29日と30日は1メートル\nでしょう。\n 福岡県の内海では、29日と30日は0.5メートルでしょう。",
"publicTime" : "2013-01-29T10:37:00+0900"
},
"link" : "http://weather.livedoor.com/area/forecast/400040",
"forecasts" : [
{
"dateLabel" : "今日",
"telop" : "晴のち曇",
"date" : "2013-01-29",
"temperature" : {
"min" : null,
"max" : {
"celsius" : "11",
"fahrenheit" : "51.8"
}
},
"image" : {
"width" : 50,
"url" : "http://weather.livedoor.com/img/icon/5.gif",
"title" : "晴のち曇",
"height" : 31
}
},
:
:
リファクタリング前
上記のJSONコードからアプリの実装に必要な値は2つだけですが、
既存のコードはJSON構文の階層をたどって値を取得するため、コードが長く読みづらいです。
override fun onPostExecute(result: String) {
//JSON文字列からJSONObjectオブジェクトを生成。これをルートJSONオブジェクトとする。
val rootJSON = JSONObject(result)
//ルートJSON直下の「description」JSONオブジェクトを取得。
val descriptionJSON = rootJSON.getJSONObject("description")
//「description」プロパティ直下の「text」文字列(天気概況文)を取得。
val desc = descriptionJSON.getString("text")
//ルートJSON直下の「forecasts」JSON配列を取得。
val forecasts = rootJSON.getJSONArray("forecasts")
//「forecasts」JSON配列のひとつ目(インデックス0)のJSONオブジェクトを取得。
val forecastNow = forecasts.getJSONObject(0)
//「forecasts」ひとつ目のJSONオブジェクトから「telop」文字列(天気)を取得。
val telop = forecastNow.getString("telop")
:
:
}
private fun is2String(stream: InputStream): String {
val sb = StringBuilder()
val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))
var line = reader.readLine()
while(line != null) {
sb.append(line)
line = reader.readLine()
}
reader.close()
return sb.toString()
}
リファクタリング後
Gsonライブラリがあれば、JSONデータをJavaオブジェクトにデシリアライズしてくれます。
というわけで、復元したコードを格納するためのクラスを作っておきます。
data class Repos(val description: Description, val forecasts : List<Forecast>)
data class Description(val text : String)
data class Forecast(val telop : String)
StreamをJSONコードに変換するis2Stringメソッドも不要です。
これだけで、随分とコードが短くできそうです。
手順3. HTTP通信のリファクタリング
今度は通信処理部分をリファクタリングしましょう。
リファクタリング前
HttpURLConnectionオブジェクトを生成して接続したらdisconnect()を呼んで切断する必要があります。
うっかり切断を忘れそうです。
override fun doInBackground(vararg params: String): String {
//可変長引数の1個目(インデックス0)を取得。これが都市ID
val id = params[0]
//都市IDを使って接続URL文字列を作成。
val urlStr = "http://weather.livedoor.com/forecast/webservice/json/v1?city=${id}"
//URLオブジェクトを生成。
val url = URL(urlStr)
//URLオブジェクトからHttpURLConnectionオブジェクトを取得。
val con = url.openConnection() as HttpURLConnection
//http接続メソッドを設定。
con.requestMethod = "GET"
:
con.disconnect()
:
:
}
リファクタリング後
接続/切断の儀式から解放されました。
HTTPのメソッドは@Get と @Query で編集できるので拡張性も良さそうな印象。
interface GitHubService {
@GET("v1")
fun fetchReposList(@Query("city") id: String): Call<Repos>
}
object APIClient {
private const val BASE_URL = "http://weather.livedoor.com/forecast/webservice/json/"
private fun restClient() : Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun fetchReposList(id : String) : Response<Repos> {
val service = restClient().create(GitHubService::class.java)
return service.fetchReposList(id).execute()
}
}
完成
上記の作ったものを繋ぎ合わせて完成。
リファクタリング前と比べると、だいぶ短くなって可読性がよくなった感じがします。
data class Repos(val description: Description, val forecasts : List<Forecast>)
data class Description(val text : String)
data class Forecast(val telop : String)
interface GitHubService {
@GET("v1")
fun fetchReposList(@Query("city") id: String): Call<Repos>
}
object APIClient {
private const val BASE_URL = "http://weather.livedoor.com/forecast/webservice/json/"
private fun restClient() : Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun fetchReposList(id : String) : Response<Repos> {
val service = restClient().create(GitHubService::class.java)
return service.fetchReposList(id).execute()
}
}
private inner class WeatherInfoReceiver(): AsyncTask<String, Repos, Repos>() {
override fun doInBackground(vararg params: String): Repos {
//可変長引数の1個目(インデックス0)を取得。これが都市ID
val id = params[0]
val response = APIClient.fetchReposList(id)
return response.body()!!
}
override fun onPostExecute(result: Repos) {
//天気情報用文字列をTextViewにセット。
val tvWeatherTelop = findViewById<TextView>(R.id.tvWeatherTelop)
val tvWeatherDesc = findViewById<TextView>(R.id.tvWeatherDesc)
tvWeatherTelop.text = result.forecasts[0].telop
tvWeatherDesc.text = result.description.text
}
}
RxJavaを使えば非同期処理部分にて、もっと短くできそうだと目論んでいます。
上手く行きそうでしたら、また記事が書きたいと思います。