Retrofit

Kotlin向けタイプセーフHTTPクライアント。アノテーションベースの宣言的API定義により、インターフェースから自動的にHTTPクライアント実装を生成。Kotlinコルーチン、サスペンド関数サポート。MoshiやGsonと統合したJSON処理。

概要

RetrofitはSquare社が開発したタイプセーフなHTTPクライアントライブラリで、Kotlin向けに最適化されています。Retrofit 3.0以降、Kotlinファーストの設計となり、コルーチンのネイティブサポートなど、モダンなKotlin開発に最適な機能を提供しています。

主な特徴

  • Kotlinコルーチンのネイティブサポート: suspend関数を使った直感的な非同期処理
  • タイプセーフなAPI定義: インターフェースとアノテーションによる安全な実装
  • Kotlinファーストの設計: null安全性とより簡潔なコード
  • 優れた拡張性: カスタムコンバーターとアダプターのサポート
  • OkHttpベース: 強力で効率的なHTTPエンジン
  • エラーハンドリングの改善: suspend関数から直接HttpExceptionをスロー

インストール

Gradle (Kotlin DSL)

dependencies {
    // Retrofit 3.0
    implementation("com.squareup.retrofit2:retrofit:3.0.0")
    // Gsonコンバーター
    implementation("com.squareup.retrofit2:converter-gson:3.0.0")
    // Moshiコンバーター(Kotlin向け推奨)
    implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
    // Kotlinx Serializationコンバーター
    implementation("com.squareup.retrofit2:converter-kotlinx-serialization:3.0.0")
}

基本的な使い方

APIインターフェースの定義(Kotlin Coroutines)

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: Long): User
    
    @GET("posts")
    suspend fun getPosts(
        @Query("page") page: Int,
        @Query("limit") limit: Int = 20
    ): List<Post>
    
    @POST("users")
    suspend fun createUser(@Body user: User): User
}

Retrofitインスタンスの作成

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

val apiService = retrofit.create<ApiService>()

実装例

1. 基本的なCRUD操作

import retrofit2.http.*
import kotlinx.coroutines.*

interface UserService {
    // GET - ユーザー取得
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Long): User
    
    // GET - ユーザー一覧
    @GET("users")
    suspend fun getUsers(
        @QueryMap options: Map<String, String> = emptyMap()
    ): List<User>
    
    // POST - ユーザー作成
    @POST("users")
    suspend fun createUser(@Body user: User): User
    
    // PUT - ユーザー更新
    @PUT("users/{id}")
    suspend fun updateUser(
        @Path("id") id: Long,
        @Body user: User
    ): User
    
    // DELETE - ユーザー削除
    @DELETE("users/{id}")
    suspend fun deleteUser(@Path("id") id: Long): Response<Unit>
}

// ViewModelでの使用例
class UserViewModel(private val userService: UserService) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users
    
    fun loadUsers() {
        viewModelScope.launch {
            try {
                val userList = userService.getUsers()
                _users.value = userList
            } catch (e: HttpException) {
                // HTTPエラー処理
                handleHttpError(e)
            } catch (e: Exception) {
                // その他のエラー処理
                handleGenericError(e)
            }
        }
    }
    
    private fun handleHttpError(e: HttpException) {
        when (e.code()) {
            401 -> // 認証エラー
            404 -> // リソースが見つからない
            500 -> // サーバーエラー
            else -> // その他のHTTPエラー
        }
    }
}

2. 認証とヘッダー管理

// 認証インターセプター
class AuthInterceptor(
    private val tokenProvider: () -> String?
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider()
        val request = chain.request().newBuilder().apply {
            token?.let {
                addHeader("Authorization", "Bearer $it")
            }
        }.build()
        
        return chain.proceed(request)
    }
}

// APIインターフェース
interface AuthService {
    @POST("auth/login")
    suspend fun login(@Body credentials: LoginRequest): LoginResponse
    
    @POST("auth/refresh")
    suspend fun refreshToken(@Body request: RefreshTokenRequest): TokenResponse
    
    @Headers("Authorization: required")
    @GET("profile")
    suspend fun getProfile(): UserProfile
}

// Retrofitの設定
fun createRetrofit(tokenProvider: () -> String?): Retrofit {
    val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(AuthInterceptor(tokenProvider))
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()
        
    return Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(okHttpClient)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
}

3. ファイルアップロード

interface FileService {
    @Multipart
    @POST("upload")
    suspend fun uploadFile(
        @Part file: MultipartBody.Part,
        @Part("description") description: RequestBody
    ): UploadResponse
    
    @Multipart
    @POST("upload/multiple")
    suspend fun uploadMultipleFiles(
        @Part files: List<MultipartBody.Part>
    ): UploadResponse
}

// ファイルアップロードの実装
class FileUploader(private val fileService: FileService) {
    suspend fun uploadImage(filePath: String, description: String): UploadResponse {
        val file = File(filePath)
        val requestFile = file.asRequestBody("image/*".toMediaType())
        val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
        val descriptionBody = description.toRequestBody("text/plain".toMediaType())
        
        return fileService.uploadFile(body, descriptionBody)
    }
    
    suspend fun uploadMultipleImages(filePaths: List<String>): UploadResponse {
        val parts = filePaths.map { path ->
            val file = File(path)
            val requestFile = file.asRequestBody("image/*".toMediaType())
            MultipartBody.Part.createFormData("files", file.name, requestFile)
        }
        
        return fileService.uploadMultipleFiles(parts)
    }
}

4. エラーハンドリングとリトライ

// カスタムエラーレスポンス
@Serializable
data class ApiError(
    val code: String,
    val message: String,
    val details: Map<String, String>? = null
)

// エラーハンドリングユーティリティ
suspend fun <T> safeApiCall(
    apiCall: suspend () -> T
): Result<T> {
    return try {
        Result.success(apiCall())
    } catch (e: HttpException) {
        val errorBody = e.response()?.errorBody()?.string()
        val apiError = errorBody?.let {
            Json.decodeFromString<ApiError>(it)
        }
        Result.failure(ApiException(e.code(), apiError))
    } catch (e: IOException) {
        Result.failure(NetworkException("Network error", e))
    } catch (e: Exception) {
        Result.failure(UnknownException("Unknown error", e))
    }
}

// リトライ機能付きの拡張関数
suspend fun <T> retryWithExponentialBackoff(
    times: Int = 3,
    initialDelay: Long = 100,
    maxDelay: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: IOException) {
            // ネットワークエラーの場合のみリトライ
        }
        delay(currentDelay)
        currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
    }
    return block() // 最後の試行
}

// 使用例
class Repository(private val apiService: ApiService) {
    suspend fun getUserWithRetry(userId: Long): Result<User> {
        return safeApiCall {
            retryWithExponentialBackoff {
                apiService.getUser(userId)
            }
        }
    }
}

5. ストリーミングとプログレス

interface DownloadService {
    @Streaming
    @GET
    suspend fun downloadFile(@Url fileUrl: String): ResponseBody
}

class FileDownloader(private val downloadService: DownloadService) {
    suspend fun downloadWithProgress(
        url: String,
        destinationFile: File,
        onProgress: (bytesRead: Long, totalBytes: Long) -> Unit
    ) = withContext(Dispatchers.IO) {
        val responseBody = downloadService.downloadFile(url)
        
        responseBody.byteStream().use { inputStream ->
            destinationFile.outputStream().use { outputStream ->
                val totalBytes = responseBody.contentLength()
                val buffer = ByteArray(8 * 1024)
                var bytesRead = 0L
                var bytes: Int
                
                while (inputStream.read(buffer).also { bytes = it } >= 0) {
                    outputStream.write(buffer, 0, bytes)
                    bytesRead += bytes
                    
                    withContext(Dispatchers.Main) {
                        onProgress(bytesRead, totalBytes)
                    }
                }
            }
        }
    }
}

6. テストとモック

// MockWebServerを使用したテスト
class ApiServiceTest {
    private lateinit var mockWebServer: MockWebServer
    private lateinit var apiService: ApiService
    
    @Before
    fun setup() {
        mockWebServer = MockWebServer()
        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
        apiService = retrofit.create()
    }
    
    @After
    fun tearDown() {
        mockWebServer.shutdown()
    }
    
    @Test
    fun `test get user success`() = runTest {
        // モックレスポンスの設定
        val mockResponse = MockResponse()
            .setBody("""{"id": 1, "name": "Test User"}""")
            .addHeader("Content-Type", "application/json")
        mockWebServer.enqueue(mockResponse)
        
        // APIコール
        val user = apiService.getUser(1)
        
        // 検証
        assertEquals(1, user.id)
        assertEquals("Test User", user.name)
        
        // リクエストの検証
        val request = mockWebServer.takeRequest()
        assertEquals("GET", request.method)
        assertEquals("/users/1", request.path)
    }
}

7. Flowを使用したリアクティブプログラミング

interface RealtimeService {
    @GET("events/stream")
    suspend fun getEventStream(): Flow<Event>
}

// Flowを返すカスタムCallAdapterFactory
class FlowCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (getRawType(returnType) != Flow::class.java) {
            return null
        }
        // 実装詳細...
        return FlowCallAdapter<Any>(responseType)
    }
}

// リポジトリでの使用
class EventRepository(private val realtimeService: RealtimeService) {
    fun observeEvents(): Flow<Event> = flow {
        while (currentCoroutineContext().isActive) {
            try {
                realtimeService.getEventStream()
                    .collect { event ->
                        emit(event)
                    }
            } catch (e: Exception) {
                // エラー処理とリトライロジック
                delay(5000) // 5秒待機してリトライ
            }
        }
    }.flowOn(Dispatchers.IO)
}

他のライブラリとの比較

Retrofit vs Ktor Client

  • Retrofit: アノテーションベース、Android標準、豊富なエコシステム
  • Ktor Client: Kotlin DSL、マルチプラットフォーム対応

Retrofit vs Fuel

  • Retrofit: タイプセーフ、大規模プロジェクト向け
  • Fuel: シンプルなAPI、小規模プロジェクト向け

ベストプラクティス

  1. 依存性注入の使用

    @Module
    @InstallIn(SingletonComponent::class)
    object NetworkModule {
        @Provides
        @Singleton
        fun provideRetrofit(): Retrofit {
            return Retrofit.Builder()
                .baseUrl("https://api.example.com/")
                .addConverterFactory(MoshiConverterFactory.create())
                .build()
        }
        
        @Provides
        @Singleton
        fun provideApiService(retrofit: Retrofit): ApiService {
            return retrofit.create()
        }
    }
    
  2. 適切なスコープの使用

    // ViewModelScope for UI-related calls
    viewModelScope.launch {
        // API call
    }
    
    // Application scope for background tasks
    GlobalScope.launch {
        // Background API call
    }
    
  3. レスポンスのキャッシング

    interface CachedApiService {
        @GET("data")
        @Headers("Cache-Control: max-age=600") // 10分間キャッシュ
        suspend fun getCachedData(): Data
    }
    

まとめ

Retrofit 3.0は、Kotlin開発者にとって最適なHTTPクライアントライブラリです。Kotlinコルーチンのネイティブサポート、改善されたエラーハンドリング、そしてタイプセーフなAPI定義により、モダンなAndroidアプリケーション開発において強力なツールとなっています。