Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

【Android】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 通信を行ってみます。

今回は、以下の GitHubAPI を使って、指定したユーザーのリポジトリ情報を取得します。

  • HTTP メソッド : GET
  • ホスト : api.github.com
  • パス : /users/{username}/repos

ライブラリのインポート

初めに、Retrofit を使えるようにするために、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
}

パーミッションの設定

次に、今回はインターネット通信を行うので、AndroidManifest.xmlandroid.permission.INTERNET を追加します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tatsuro.app.retrofitsample02">

    <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 に出力されます。

参考