OkHttp

Kotlin/Java向けの高性能HTTPクライアント。Square社開発でAndroid公式推奨。HTTP/2サポート、接続プーリング、GZIP圧縮、レスポンスキャッシング、ネットワーク障害からの自動復旧機能を内蔵。Kotlinコルーチンと相性が良く、現代的な非同期処理に最適化。

概要

OkHttpは、Square社が開発した効率的なHTTPクライアントライブラリです。元々はJava向けに作られましたが、現在ではKotlinファーストの設計となっており、モダンなHTTP通信の実装に必要な機能を網羅しています。

主な特徴

  • 接続プーリング: HTTP/2とHTTP/1.1の接続を効率的に再利用
  • 透過的なGZIP圧縮: 帯域幅を削減してレスポンスを高速化
  • レスポンスキャッシング: 不要なネットワークリクエストを削減
  • WebSocketサポート: リアルタイム通信をネイティブサポート
  • インターセプター: リクエスト/レスポンスの監視と変更
  • 自動リトライとリダイレクト: 堅牢な通信の実現
  • Kotlinファースト: OkHttp 5.0以降、Kotlin向けの最適化

インストール

Gradle (Kotlin DSL)

dependencies {
    // OkHttp 5.0 (最新版)
    implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.12")
    // ロギングインターセプター
    implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12")
    // MockWebServer (テスト用)
    testImplementation("com.squareup.okhttp3:mockwebserver:5.0.0-alpha.12")
}

Maven

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>5.0.0-alpha.12</version>
</dependency>

基本的な使い方

クライアントの作成

// デフォルト設定のクライアント
val client = OkHttpClient()

// カスタマイズされたクライアント
val customClient = OkHttpClient.Builder()
    .connectTimeout(Duration.ofSeconds(10))
    .readTimeout(Duration.ofSeconds(30))
    .writeTimeout(Duration.ofSeconds(15))
    .build()

シンプルなリクエスト

// OkHttp 5.0の新しいKotlin向けAPI
val request = Request(
    url = "https://api.example.com/data".toHttpUrl()
)

// 従来のビルダーパターン
val requestBuilder = Request.Builder()
    .url("https://api.example.com/data")
    .build()

実装例

1. GETリクエスト

import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException

class ApiClient {
    private val client = OkHttpClient()
    
    // 同期的なGETリクエスト
    fun getSync(url: String): String? {
        val request = Request(url = url.toHttpUrl())
        
        return try {
            client.newCall(request).execute().use { response ->
                if (response.isSuccessful) {
                    response.body?.string()
                } else {
                    throw IOException("Unexpected code $response")
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
            null
        }
    }
    
    // 非同期GETリクエスト
    fun getAsync(url: String, callback: (String?) -> Unit) {
        val request = Request(url = url.toHttpUrl())
        
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback(null)
            }
            
            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (response.isSuccessful) {
                        callback(response.body?.string())
                    } else {
                        callback(null)
                    }
                }
            }
        })
    }
}

2. POSTリクエスト

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.FormBody
import okhttp3.MultipartBody

class PostRequests {
    private val client = OkHttpClient()
    
    // JSON POSTリクエスト
    fun postJson(url: String, json: String): Response {
        val mediaType = "application/json; charset=utf-8".toMediaType()
        val body = json.toRequestBody(mediaType)
        
        val request = Request.Builder()
            .url(url)
            .post(body)
            .build()
            
        return client.newCall(request).execute()
    }
    
    // フォームデータの送信
    fun postForm(url: String, params: Map<String, String>): Response {
        val formBody = FormBody.Builder().apply {
            params.forEach { (key, value) ->
                add(key, value)
            }
        }.build()
        
        val request = Request.Builder()
            .url(url)
            .post(formBody)
            .build()
            
        return client.newCall(request).execute()
    }
    
    // マルチパートリクエスト
    fun postMultipart(url: String, filePath: String, description: String): Response {
        val file = File(filePath)
        val fileBody = file.asRequestBody("image/jpeg".toMediaType())
        
        val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("file", file.name, fileBody)
            .addFormDataPart("description", description)
            .build()
            
        val request = Request.Builder()
            .url(url)
            .post(requestBody)
            .build()
            
        return client.newCall(request).execute()
    }
}

3. インターセプターの実装

import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor

class InterceptorExamples {
    // 認証インターセプター
    class AuthInterceptor(private val token: String) : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val originalRequest = chain.request()
            val newRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
                
            return chain.proceed(newRequest)
        }
    }
    
    // リトライインターセプター
    class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val request = chain.request()
            var response: Response? = null
            var exception: IOException? = null
            
            for (i in 0 until maxRetries) {
                try {
                    response = chain.proceed(request)
                    if (response.isSuccessful) return response
                    response.close()
                } catch (e: IOException) {
                    exception = e
                }
                
                // 待機時間を設定(指数バックオフ)
                Thread.sleep((2.0.pow(i) * 1000).toLong())
            }
            
            throw exception ?: IOException("Max retries exceeded")
        }
    }
    
    // クライアントへの追加
    fun createClient(token: String): OkHttpClient {
        val loggingInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }
        
        return OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor(token))
            .addInterceptor(RetryInterceptor())
            .addInterceptor(loggingInterceptor)
            .build()
    }
}

4. WebSocketの実装

import okhttp3.WebSocket
import okhttp3.WebSocketListener

class WebSocketExample {
    private val client = OkHttpClient.Builder()
        .readTimeout(0, TimeUnit.MILLISECONDS)
        .build()
        
    fun connectWebSocket(url: String) {
        val request = Request.Builder()
            .url(url)
            .build()
            
        val listener = object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                println("WebSocket接続が開かれました")
                // メッセージを送信
                webSocket.send("Hello Server!")
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                println("受信したメッセージ: $text")
            }
            
            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                println("受信したバイナリ: ${bytes.hex()}")
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                println("WebSocketが閉じられます: $code / $reason")
                webSocket.close(1000, null)
            }
            
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                println("WebSocketエラー: ${t.message}")
            }
        }
        
        client.newWebSocket(request, listener)
    }
}

5. 接続プーリングとキャッシング

import okhttp3.Cache
import okhttp3.ConnectionPool
import okhttp3.CacheControl
import java.util.concurrent.TimeUnit

class PerformanceOptimization {
    // カスタム接続プールの設定
    private val connectionPool = ConnectionPool(
        maxIdleConnections = 5,
        keepAliveDuration = 5,
        timeUnit = TimeUnit.MINUTES
    )
    
    // キャッシュの設定(10MB)
    private val cacheDir = File(System.getProperty("java.io.tmpdir"), "okhttp-cache")
    private val cache = Cache(cacheDir, 10 * 1024 * 1024) // 10MB
    
    val optimizedClient = OkHttpClient.Builder()
        .connectionPool(connectionPool)
        .cache(cache)
        .build()
        
    // キャッシュ制御の例
    fun requestWithCacheControl(url: String) {
        // キャッシュを強制的に使用
        val forceCache = Request.Builder()
            .url(url)
            .cacheControl(CacheControl.FORCE_CACHE)
            .build()
            
        // キャッシュを無視
        val noCache = Request.Builder()
            .url(url)
            .cacheControl(CacheControl.FORCE_NETWORK)
            .build()
            
        // 最大経過時間を指定
        val maxAge = Request.Builder()
            .url(url)
            .cacheControl(
                CacheControl.Builder()
                    .maxAge(10, TimeUnit.MINUTES)
                    .build()
            )
            .build()
    }
}

6. ファイルのダウンロードと進捗管理

import okhttp3.ResponseBody
import java.io.FileOutputStream

class FileDownloader {
    private val client = OkHttpClient()
    
    fun downloadFile(
        url: String,
        destinationFile: File,
        progressListener: (bytesRead: Long, contentLength: Long) -> Unit
    ) {
        val request = Request.Builder()
            .url(url)
            .build()
            
        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("Unexpected code $response")
            
            val body = response.body ?: throw IOException("Response body is null")
            val contentLength = body.contentLength()
            
            body.source().use { source ->
                FileOutputStream(destinationFile).use { output ->
                    val buffer = ByteArray(8192)
                    var totalBytesRead = 0L
                    var bytesRead: Int
                    
                    while (source.read(buffer).also { bytesRead = it } != -1) {
                        output.write(buffer, 0, bytesRead)
                        totalBytesRead += bytesRead
                        progressListener(totalBytesRead, contentLength)
                    }
                }
            }
        }
    }
}

7. コルーチンとの統合

import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

// OkHttpをコルーチンで使用するための拡張関数
suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation ->
    enqueue(object : Callback {
        override fun onResponse(call: Call, response: Response) {
            continuation.resume(response)
        }
        
        override fun onFailure(call: Call, e: IOException) {
            continuation.resumeWithException(e)
        }
    })
    
    continuation.invokeOnCancellation {
        cancel()
    }
}

// 使用例
class CoroutineApiClient {
    private val client = OkHttpClient()
    
    suspend fun getData(url: String): String = withContext(Dispatchers.IO) {
        val request = Request(url = url.toHttpUrl())
        
        client.newCall(request).await().use { response ->
            if (response.isSuccessful) {
                response.body?.string() ?: throw IOException("Empty response body")
            } else {
                throw IOException("Unexpected code $response")
            }
        }
    }
}

他のライブラリとの比較

OkHttp vs Retrofit

  • OkHttp: 低レベルHTTPクライアント、細かい制御が可能
  • Retrofit: OkHttpをベースにした高レベルRESTクライアント

OkHttp vs Ktor Client

  • OkHttp: 成熟したライブラリ、Android標準
  • Ktor Client: Kotlin専用、マルチプラットフォーム対応

OkHttp vs Fuel

  • OkHttp: より多機能、エンタープライズ向け
  • Fuel: シンプルなAPI、小規模プロジェクト向け

ベストプラクティス

  1. クライアントの再利用

    // シングルトンパターンの実装
    object HttpClient {
        val instance: OkHttpClient by lazy {
            OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build()
        }
    }
    
  2. 適切なタイムアウトの設定

    val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()
    
  3. エラーハンドリングの実装

    sealed class NetworkResult<T> {
        data class Success<T>(val data: T) : NetworkResult<T>()
        data class Error<T>(val exception: Exception) : NetworkResult<T>()
    }
    
    suspend fun <T> safeApiCall(
        apiCall: suspend () -> T
    ): NetworkResult<T> {
        return try {
            NetworkResult.Success(apiCall())
        } catch (e: Exception) {
            NetworkResult.Error(e)
        }
    }
    

まとめ

OkHttpは、Kotlinアプリケーションにおける強力で効率的なHTTPクライアントライブラリです。接続プーリング、キャッシング、WebSocketサポートなどの高度な機能により、あらゆる規模のアプリケーションに対応できます。特にAndroid開発では事実上の標準となっており、Retrofitなどの人気ライブラリの基盤としても使われています。