はじめに
Firebase Authentication の ID トークンを HTTP ヘッダに付与したり、独自データと JSON 文字列間の変換を行ったりすることはよくあるのですが、頻繁に使う割に忘れるので記事として残します。
環境
build.gradle は次のような内容になっています。
plugins {
    ...
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.21'
    id 'com.google.gms.google-services'
}
dependencies {
    ...
    implementation "androidx.navigation:navigation-compose:2.4.0-alpha06"
    implementation platform('com.google.firebase:firebase-bom:28.4.0')
    implementation 'com.google.firebase:firebase-auth-ktx'
    implementation 'com.squareup.okhttp3:okhttp:4.9.0'
}
実装
- POST で送信
 - Firebase Authentication の ID トークンを HTTP ヘッダに付与
 - Firebase Authentication の認証が終えていない場合は例外を投げる
 - Serializable なクラスのインスタンスを JSON 文字列にエンコード
 - API サーバーから受け取った JSON 文字列をクラスのインスタンスにデコード
 
上記の仕様を持つ request 関数は次のようになります。
val API_PREFIX = "http://192.168.1.100:3000/api"
val json = Json { ignoreUnknownKeys = true }
@OptIn(ExperimentalSerializationApi::class)
inline fun <reified T, reified S> request(path: String, post: T): S {
  val user = FirebaseAuth.getInstance().currentUser
  require(user != null)
  val idToken = Tasks.await(user.getIdToken(true)).token
  require(idToken != null)
  val client = OkHttpClient()
  val req = Request.Builder().apply {
    addHeader("Authorization", "Bearer $idToken")
    url("${API_PREFIX}${path}")
    post(json.encodeToString(post).toRequestBody("application/json".toMediaType()))
  }.build()
  client.newCall(req).execute().use {
    val str = it.body?.string()
    require(str != null)
    return json.decodeFromString(str)
  }
}
正直なところ、inline や reified を使う理由がよくわかりません。コンパイルエラーを直していくと上記のコードになりました。
使用例
次のように使用します。Kotlin の型推論により、戻り値の型を request 関数で指定する必要がなくて素晴らしいです。
@Serializable
data class ListUsersRequest(
  val offset: Int,
  val pageSize: Int,
)
@Serializable
data class User(val id: Int, val name: String)
fun listUsers(req: ListUsersRequest): List<User> {
  return request("/users/list", req)
}
この関数は Jetpack Compose の Composable から使うことを想定しています。ボタンがクリックされたときなどです。その箇所のコードは次のようになります。
var users by remember { mutableStateOf<List<User>>(emptyList()) }
var ajax by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val handleClick = {
  scope.launch(Dispatchers.IO) { 
    try {
      ajax = true
      val req = ListUsersRequest(0, 20)
      val res = listUsers(req)
      users = res
    } catch (e: Exception) {
      // TODO: ここで Snackbar などを使ってエラー内容をユーザーに伝える
      Log.e("ERROR", e.toString())
      return@launch
    } finally {
      ajax = false
    }
  }
  Unit
}
Button(onClick = handleClick, enabled = !ajax) {
  Text("click me!")
}
おわりに
HTTP リクエストを行うライブラリを公式が用意してくれると嬉しいのですが、ないですかね?