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、小規模プロジェクト向け
ベストプラクティス
-
依存性注入の使用
@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() } } -
適切なスコープの使用
// ViewModelScope for UI-related calls viewModelScope.launch { // API call } // Application scope for background tasks GlobalScope.launch { // Background API call } -
レスポンスのキャッシング
interface CachedApiService { @GET("data") @Headers("Cache-Control: max-age=600") // 10分間キャッシュ suspend fun getCachedData(): Data }
まとめ
Retrofit 3.0は、Kotlin開発者にとって最適なHTTPクライアントライブラリです。Kotlinコルーチンのネイティブサポート、改善されたエラーハンドリング、そしてタイプセーフなAPI定義により、モダンなAndroidアプリケーション開発において強力なツールとなっています。