LoginSignup
3
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-01

Androidアプリ開発の教科書」の11章「非同期処理とWeb API連携」のサンプルコードを
Web API連携でよく使われているライブラリ(Retrofit2 + Gson + Okhttp)でリファクタリングしてみました。

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

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

image.png

リファクタリング対象のコードはこちら↓
実行できる環境が欲しい方はこちらからダウンロードできます。

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

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

{
   "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構文の階層をたどって値を取得するため、コードが長く読みづらいです。

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


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

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

    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()
        }
    }

完成

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

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

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4