【Android】Retrofit使ってみた

# はじめに
最近自分の周りでよく聞くRetrofitを使用して、API通信で取得したデータをAndroidアプリの画面に表示させてみました。
本記事はそのサンプルコード及び備忘になります。
変な箇所とかあれば、遠慮なく指摘いただけますと嬉しいです。

:::note info
Retrofitとは
HTTPクライアント通信を行うためのライブラリです。
これによりアプリからサーバと通信して情報を取得することが可能です。
:::

# 実装
実装について記載していきます。

**API準備**
コーディングに移る前にまずは取得するAPIの準備をします。
今回は[OpenWeather](https://openweathermap.org/)という天気情報を取得できるAPIを使用して実装していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308202/f7f31417-16d0-e65b-0751-5202f24d4c92.png)

**Manifest**
天気APIを使用する際はインターネット通信を行うため、以下をManifestに追記します。
```Kotlin:AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
```

**build.gradle**
Retrofitを使用するために、以下をbuild.gradle（app）に追記します。
```Kotlin:build.gradle
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
```

**モデル**
天気情報（場所・天気）を格納するモデルを用意します。
```Kotlin:WeatherModel.kt
data class WeatherModel(
    val placeName: String,
    val weatherInfo: String,
) {
    val place: String = placeName
    val weather: String = weatherInfo
}
```
**APIインターフェース**
Retrofitで使用するAPIのインターフェースを用意します。
@QueryではURLに渡すパラメータを指定しています。
```Kotlin:WeatherService.kt
interface WeatherService {
    //生成するURL：https://api.openweathermap.org/data/2.5/weather?lang=ja&q=tokyo&appid=*****************
    @GET("data/2.5/weather")
    fun loadRepos(
        @Query("lang") lang: String,
        @Query("q") q: String,
        @Query("appid") appid: String
    ): Call<ResponseBody>
}
```

**DataSource**
天気APIを叩いて情報取得する処理を実装します。
API通信して取得したデータはJsonにして、先ほど作成したモデルを更新します。
```Kotlin:WeatherRemoteDataSource.kt
class WeatherRemoteDataSource {
    //APIキーとURLの定義
    private val apiKey: String = "*****************"
    private val mainURL: String = "https://api.openweathermap.org"

    private val retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(mainURL)
        .build()

    private val service = retrofit.create(WeatherService::class.java)
    private val loadRepos = service.loadRepos("ja", "tokyo", apiKey)

    fun featchWeather(): WeatherModel? = featchWeatherInfo()

    private fun featchWeatherInfo(): WeatherModel? {
        runCatching {
            loadRepos.clone().execute()
        }.onSuccess { responce ->
            if (responce.isSuccessful) {
                responce.body()?.string()?.let {
                    val jsonObj = JSONObject(it)

                    // モデルに取得したJsonの値を代入
                    return WeatherModel(
                        jsonObj.getString("name"),
                        jsonObj.getJSONArray("weather").getJSONObject(0).getString("description"),
                    )
                }
            } else {
                return null
            }
        }.onFailure {
            return null
        }
        return null
    }
}
```

**Repository**
DataSourceで更新したモデル情報を渡すRepositoryを用意します。
```Kotlin:WeatherRepository.kt
class WeatherRepository(
    private val weatherRemoteDataSource: WeatherRemoteDataSource
) : IWeatherRepository {
    override suspend fun featchWeather(): WeatherModel? = weatherRemoteDataSource.featchWeather()
}
```

RepositoryのInterfaceも用意しました。
```Kotlin:IWeatherRepository.kt
interface IWeatherRepository {
    suspend fun featchWeather(): WeatherModel?
}
```

**Presenter**
Viewに取得データを渡すPresenterを用意します。
```Kotlin:WeatherPresenter.kt
class WeatherPresenter(
    private val view: WeatherContract.View,
) : WeatherContract.Presenter {

    // Repositoryを指定
    private var weaherRepository: WeatherRepository = WeatherRepository(WeatherRemoteDataSource())

    // アプリ起動したら天気情報取得を実施
    init {
        getWeather()
    }

    // 天気取得処理（取得成功したら取得情報をUIに反映し、失敗したら失敗ダイアログを表示）
    override fun getWeather() {
        var result: WeatherModel? = null
        CoroutineScope(Dispatchers.IO).launch {
            runCatching {
                withContext(Dispatchers.Main) {
                    view.setProgressBar(true)
                }
                result = weaherRepository.featchWeather()
            }.onSuccess {
                result?.let {
                    withContext(Dispatchers.Main) {
                        view.setProgressBar(false)
                        view.setWeatherInfo(it)
                    }
                    return@let
                }
            }.onFailure {
                withContext(Dispatchers.Main) {
                    view.showDialog()
                    view.setProgressBar(false)
                }
            }
        }
    }
}
```

ViewとPresenterのやりとりを定義したinterfaceを用意します。
```Kotlin:WeatherContract.kt
interface WeatherContract {
    interface View {
        fun setWeatherInfo(weatherModel: WeatherModel)
        fun showDialog()
        fun setProgressBar(isShow: Boolean)
    }

    interface Presenter {
        fun getWeather()
    }
}
```
**Activity**
Activityクラスを用意します。
Presenterから受け取った天気情報をUIに表示します。
```Kotlin:MainActivity.kt
class MainActivity : AppCompatActivity(), WeatherContract.View {

    // Presenterを指定
    private lateinit var presenter: WeatherContract.Presenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        presenter = WeatherPresenter(this)

        val btnGetWeather: Button = findViewById(R.id.btnGetWeather)
        val btnClear: Button = findViewById(R.id.btnClear)

        //東京ボタンクリック時の処理
        btnGetWeather.setOnClickListener {
            presenter.getWeather()
        }

        //クリアボタンクリック時の処理
        btnClear.setOnClickListener {
            val place: TextView = findViewById(R.id.placeName)
            val weather: TextView = findViewById(R.id.weather)
            place.text = "場所"
            weather.text = "天気"
        }
    }

    //画面のテキストにJsonの取得結果（天気情報）を表示
    override fun setWeatherInfo(weatherModel: WeatherModel) {
        val place: TextView = findViewById(R.id.placeName)
        val weather: TextView = findViewById(R.id.weather)
        place.text = "${weatherModel.place}の天気は"
        weather.text = "${weatherModel.weather}です。"
    }

    // 取得失敗時のエラーダイアログ表示処理
    override fun showDialog() {
        AlertDialog.Builder(this)
            .setTitle("取得失敗")
            .setMessage("天気情報の取得に失敗しました")
            .setPositiveButton("OK") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }

    // 情報取得中のプログレスバー表示処理
    override fun setProgressBar(isShow: Boolean) {
        val progressBar: ProgressBar = findViewById(R.id.progressBar)
        progressBar.isVisible = isShow
    }
}
```
# 動作確認
「天気情報取得」で天気情報を取得し、「初期化」で表示を初期化します。
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308202/aede8b82-e07a-ea89-1779-e4488b8bcfd9.gif)

# つまづいたところ
実装の中でいくつかつまづいたので備忘として書いていきます。
**・API準備**
APIのインタフェースを用意するとき、構築するURLのクエリ(?以降)の指定に地味に引っかかりました。@Queryをつけてなかったり、クエリ名を間違えていて正しいURLになってなかったり。

↓の形でクエリ名を指定するとうまく動きました。
```Kotlin
// 構築URL：https://sample.com/test?a=***&b=***&c=***
// ↑のクエリ(?以降)を指定する書き方
@Query("a") a: String,
@Query("b") b: String,
@Query("c") c: String
```

**・Jsonのパースについて**
取得したJsonのパースが失敗していたため、Gsonというライブラリを使ってみました。
これでjsonをjava（Kotlin）で扱えるようになります。

```Kotlin
    private val retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())　// ここでGsonを使用
        .baseUrl(mainURL)
        .build()
 ```       

# 参考
https://square.github.io/retrofit/
https://smile-jsp.hateblo.jp/entry/android/retrofit-basic
https://qiita.com/kaba/items/355363645b8a9e47e2da