【Android × Kotlin】Retrofit で HTTP 通信を行う
今回は、Retrofit を使って HTTP 通信を行う方法を解説します。
なお、ここに掲載しているソースコードは以下の環境で動作確認しています。
- Android Studio Bumblebee | 2021.1.1 Patch 3
- JDK 11.0.11
- Android Gradle Plugin 7.1.3
- Kotlin 1.6.21
- Gradle 7.4.2
- com.squareup.retrofit2 2.9.0
Retrofit とは?
Retrofit は HTTP クライアント通信ができるライブラリです。使用できる HTTP メソッドは、GET、POST、PUT、PATCH、DELETE、OPTIONS、HEAD です。
Retrofit では、使用する API をインターフェースとアノテーションにて定義します。
なお、Retrofit の実態は OkHttp のラッパーであり、OkHttpClient を使用して機能を拡張することができます。例えば、HTTP 通信の送受信内容を logcat に表示するができます。
Retrofit で GitHub リポジトリの情報を取得する
実際に Retrofit にて HTTP 通信を行ってみます。
今回は、以下の GitHub の API を使って、指定したユーザーのリポジトリ情報を取得します。
ライブラリのインポート
初めに、Retrofit を使えるようにするために、アプリの build.gradle ファイルに次の依存関係を追加します。
dependencies {
implementation "com.squareup.retrofit2:retrofit:2.9.0"
}
パーミッションの設定
次に、今回はインターネット通信を行うので、AndroidManifest.xml に android.permission.INTERNET
を追加します。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tatsuro.app.retrofitsample"> <uses-permission android:name="android.permission.INTERNET" /> ︙ </manifest>
API インターフェースの定義
ここから実装に入っていきます。
まず、次のように使用する API を定義します。
interface GitHubService { @GET("users/{username}/repos") fun loadRepos( @Path("username") username: String, @Query("sort") sort: String ): Call<ResponseBody> }
API を定義するために、インターフェースを宣言します。
interface GitHubService {
︙
}
そして、使用する API 1 つに対して、メソッドを 1 つ追加します。
@GET("users/{username}/repos") fun loadRepos( @Path("username") username: String, @Query("sort") sort: String ): Call<ResponseBody>
追加したメソッドには、次のように使用する API のメソッドに合わせてアノテーションを追加します。
@GET("users/{username}/repos")
今回は GET メソッドなので、@GET を追加します。そして、@GET の引数に 今回使用する API のパスを設定します。このとき、API の中の username
パラメータは実際に API を呼び出すときに後から設定できるようにするために、波括弧で囲います。
そして、メソッドに引数を追加します。
まず、次のようにAPI の中の username
パラメータを引数に追加します。
@Path("username") username: String
この引数には、それがパスの一部であることを表す @Path を追加して、その引数にパタメータの名称 "username"
を設定します。
次に、API のクエリを引数に追加します。
@Query("sort") sort: String
今回は、sort
クエリを引数に追加します。この引数には、それがクエリであることを表す @Query を追加して、その引数にクエリの名称 "sort"
を設定します。
最後に、メソッドに戻り値 Call<ResponseBody>
を設定します。実際には、この Call インスタンスを使用して API を呼び出し、その結果を受け取ります。
以上で、API インターフェースの定義は完了です。
HTTP 通信を行う
さきほど定義したメソッドを使って、HTTP 通信の API を呼び出します。
Retrofit で HTTP 通信を行う場合、同期方式で HTTP 通信を行う Call#execute と 非同期方式で HTTP 通信を行う Call#enqueue があります。
メソッド | 方式 |
---|---|
execute | 同期 |
enqueue | 非同期 |
ここでは、これら 2 つの方式で HTTP 通信を行います。
同期方式で HTTP 通信を行う
同期方式 Call#execute で HTTP 通信を行う場合、以下のように実装します。
class MainActivity : AppCompatActivity(R.layout.main_activity) { companion object { private const val TAG = "MainActivity" private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build() private val service = retrofit.create(GitHubService::class.java) private val loadRepos = service.loadRepos("octocat", "created") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) } } } }
まず、Retrofit のインスタンスを作成します。このインスタンスには、API のホストを渡す必要があります。
companion object { ︙ private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build()
次に、API インターフェースの実体を作成します。Retrofit#create に、さきほど定義した API インターフェース を渡し、API インターフェースの実体を取得します。
private val service = retrofit.create(GitHubService::class.java)
そして、API の Call インスタンスを取得します。このとき、API にユーザ名とソートのパラメータを設定します。
private val loadRepos = service.loadRepos("octocat", "created")
Call インスタンスを取得できたので、Call#execute を使って API を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) } } }
まず、UI スレッド上で Retrofit を使って同期通信を行うことはできないので、UI スレッドとは別のスレッドを作成します。また、Call インスタンスは、連続で同じ API を呼び出すことができないので、Call#execute を呼ぶ前に Call#clone を使い、API 呼び出しのたびに、新しい Call インスタンスを生成するようにします。
private fun executeReposLoad() { thread { runCatching { loadRepos.clone().execute() } ︙
API 呼び出しは通信不良などで失敗する場合があり、その場合は Call#execute が例外を発します。よって、今回は、runCatching を使って Call#execute が発する例外をキャッチできるようにします。
そして、API 呼び出し成功と失敗それぞれに対して、その結果を Logcat に出力します。
なお、Call#execute が例外を発しなかった場合でも、HTTP のステータスコードが 404 または 500 の場合があるため、Response#isSuccessful で HTTP 通信が成功であることを確認します。
runCatching { loadRepos.clone().execute() } .onSuccess { response -> if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } .onFailure { t -> Log.e(TAG, t.toString()) }
以上の API 呼び出しを行うと、その結果が Logcat に出力されます。
非同期方式で HTTP 通信を行う
非同期方式 Call#enqueue で HTTP 通信を行う場合、以下のように実装します。
class MainActivity : AppCompatActivity(R.layout.main_activity) { companion object { private const val TAG = "MainActivity" private const val BASE_URL = "https://api.github.com" } private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .build() private val service = retrofit.create(GitHubService::class.java) private val loadRepos = service.loadRepos("tatsuroappdev") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findViewById<Button>(R.id.button) .setOnClickListener { executeReposLoad() } } private fun executeReposLoad() { loadRepos.clone().enqueue(object : Callback<ResponseBody> { override fun onResponse( call: Call<ResponseBody>, response: Response<ResponseBody> ) { if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } override fun onFailure( call: Call<ResponseBody>, t: Throwable ) { Log.e(TAG, t.toString()) } }) } }
Call インスタンスを取得するまでの流れは、同期方式のときと同じであるため、解説を省略します。また、API 呼び出しのたびに、Call#clone を使って、新しい Call インスタンスを生成するのも、同期方式のときと同様に行います。
Call#enqueue を使って API 呼び出しを行う場合、API 呼び出し後に呼び出される Callback を定義して、呼び出し後の処理を実装します。
Callback#onResponse は、通信が成功した場合に呼び出されます。処理の中身は、同期方式のときの Result.onSuccess と同じにします。
Callback#onFailure は、通信が失敗した場合に呼び出されます。処理の中身は、同期方式のときの Result.onFailure と同じにします。
loadRepos.clone().enqueue(object : Callback<ResponseBody> { override fun onResponse( call: Call<ResponseBody>, response: Response<ResponseBody> ) { if (response.isSuccessful) { response.body()?.string()?.let { json -> Log.d(TAG, json) } } else { val msg = "HTTP error. HTTP status code: ${response.code()}" Log.e(TAG, msg) } } override fun onFailure( call: Call<ResponseBody>, t: Throwable ) { Log.e(TAG, t.toString()) } })
以上の API 呼び出しを行うと、その結果が Logcat に出力されます。