[Android] 014. HTTP通信(HttpURLConnection)

Jetpack ComopseでHTTP通信処理のいい感じのコードが転がってなかったので作ってみました
Kotlin独自のAPIなどは無いようなので今回はJavaのHttpURLConnectionを使います
OkHttp3などもありますが保守性のためなるべくサードベンダー製は使用しない方向です

developerではRetrofitを使用した例が紹介されています
https://developer.android.com/courses/pathways/android-basics-compose-unit-5-pathway-1?hl=ja#codelab-https://developer.android.com/codelabs/basic-android-kotlin-compose-getting-data-internet

他で使う場合再度説明がめんどいのでAndroid LibraryにしてGitHubにあげておきました
https://github.com/bps-e/HttpClient

とりあえずgetだけ
実務で使う場合は通信キャンセルやマルチパート対応やベーシック認証の処理など追加すると思います

/**
 * ```AndroidManifest.xml
 * <manifest>
 *     <uses-permission android:name="android.permission.INTERNET" />
 * ```
 */
class HttpClient {
    private suspend fun sendRequest(
        url: String,
        timeout: Int = 5 * 1000,
        onError: (e: Exception) -> Unit,
        onCompleted: (responseCode: Int, responseData: ByteArray) -> Unit
    ) = coroutineScope {
        withContext(Dispatchers.IO) {
            try {
                val connection = URL(url).openConnection() as HttpURLConnection
                connection?.apply {
                    requestMethod = "GET"
                    connectTimeout = timeout
                    readTimeout = timeout

                    // connect()はgetResponseCode()を使用する場合は省略可 ※ここではresponseCode
                    connect()
                    yield()

                    val stream = if (responseCode == HttpURLConnection.HTTP_OK) inputStream else errorStream
                    val responseData = ByteArrayOutputStream()
                    try {
                        val buf = ByteArray(1024)
                        var size: Int
                        while (stream.read(buf).also { size = it } != -1) {
                            responseData.write(buf, 0, size)
                        }
                        responseData.flush()
                    }
                    catch (e: Exception) {
                        throw e
                    }
                    finally {
                        // JDK7からAutoCloseableになったためclose()不要
                        onCompleted(responseCode, responseData.toByteArray())
                        connection.disconnect()
                    }
                }
            }
            catch (e: Exception) {
                onError(e)
            }
        }
    }

    companion object {
        const val OK = HttpURLConnection.HTTP_OK

        suspend fun Get(
            url: String,
            timeout: Int = 5 * 1000,
            onError: (e: Exception) -> Unit = {},
            onCompleted: (responseCode: Int, responseData: ByteArray) -> Unit
        ) {
            HttpClient().sendRequest(url, timeout = timeout, onError = onError, onCompleted = onCompleted)
        }
    }
}

使い方はこんな感じです

val scope = rememberCoroutineScope()
var job: Job? by remember { mutableStateOf(null) }
val url = "http://abehiroshi.la.coocan.jp/"

Column(Modifier.fillMaxSize()) {
    Button(onClick = {
        if (job == null || job!!.isCompleted || job!!.isCancelled) {
                job = scope.launch {
                    HttpClient.Get(url) { code, data ->
                        if (code == HttpClient.OK) {
                            text = String(data, Charsets.UTF_8)
                        }
                    }
                }
            }
        }
    }) {
        Text("get")
    }
}

軽量で有名な阿部さんのサイトに接続してみます
阿部さんのサイトはHTTPSに対応してないので例外を許可する以下を追加します

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">abehiroshi.la.coocan.jp</domain>
    </domain-config>
</network-security-config>
<manifest>
<application 
    android:networkSecurityConfig="@xml/network_security_config"
>

Android Studio Giraffe 2022.3.1 built on June 29, 2023