書籍「Androidアプリ開発の教科書」 の11章をRetrofit2でリファクタリング

「[Androidアプリ開発の教科書](https://www.amazon.co.jp/dp/B078X8H61T/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)」の11章「非同期処理とWeb API連携」のサンプルコードを
Web API連携でよく使われているライブラリ(Retrofit2 + Gson + Okhttp)でリファクタリングしてみました。

- Gson・・・JavaオブジェクトとJSONデータをシリアライズ／デシリアライズするライブラリ
- okhttp・・・HTTPおよびHTTP2のネットワーククライアントのライブラリ
- Retrofit2・・・ネットワーク通信のためのライブラリ

本書のサンプルはリストにある地域をタッチすると[livedoor天気情報](http://weather.livedoor.com/weather_hacks/webservice)のWebAPIを使って、
天気情報を表示するアプリです。
学習目的でサンプルを改造した過程を記事にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291470/8d2d5ecc-df1b-739f-80c5-055cba6b76c1.png)

リファクタリング対象のコードはこちら↓
実行できる環境が欲しい方は[こちら](https://wings.msn.to/index.php/-/A-03/978-4-7981-6044-3/)からダウンロードできます。

```kotlin:WeatherInfoReceiver.kt

	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のライブラリが使えるようにします

```gradle:build.gradle

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コードを返します。

```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コードからアプリの実装に必要な値は２つだけですが、
既存のコードはJSON構文の階層をたどって値を取得するため、コードが長く読みづらいです。

```kotlin:WeatherInfoReceiver.class

		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オブジェクトにデシリアライズしてくれます。
というわけで、復元したコードを格納するためのクラスを作っておきます。

```kotlin:

    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()を呼んで切断する必要があります。
うっかり切断を忘れそうです。

```kotlin:WeatherInfoReceiver.class

		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 で編集できるので拡張性も良さそうな印象。

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

```

## 完成

上記の作ったものを繋ぎ合わせて完成。
リファクタリング前と比べると、だいぶ短くなって可読性がよくなった感じがします。

```kotlin:MainActivity.kt


	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を使えば非同期処理部分にて、もっと短くできそうだと目論んでいます。
上手く行きそうでしたら、また記事が書きたいと思います。