Fuel

Kotlin専用設計のHTTPネットワークライブラリ。Kotlinらしい慣用的API、拡張関数活用、コルーチンサポート。シンプルで読みやすい構文、RxJavaサポート、テスト支援機能を提供。Kotlinの言語特性を活かした直感的な開発体験を実現。

HTTPクライアントKotlinマルチプラットフォームコルーチン非同期

GitHub概要

kittinunf/fuel

The easiest HTTP networking library for Kotlin/Android

スター4,655
ウォッチ75
フォーク435
作成日:2015年6月4日
言語:Kotlin
ライセンス:MIT License

トピックス

androidhttp-clientkotlinnetworkingrestrxjava

スター履歴

kittinunf/fuel Star History
データ取得日時: 2025/10/22 09:55

ライブラリ

Fuel

概要

FuelはKotlin/Android向けに設計された「最も簡単なHTTPネットワーキングライブラリ」で、Kotlinx Coroutinesによってサポートされた現代的なHTTPクライアントです。Kotlinマルチプラットフォーム対応により、JVM、Android、Apple、WASMプラットフォーム間で一貫したHTTP通信APIを提供。シンプルで直感的な文字列拡張関数から高度なFuelBuilderまで、柔軟性のある設計でプロジェクトの規模や複雑さに応じて適切なアプローチを選択できます。コルーチンファーストの設計により、非同期・ノンブロッキングなHTTP処理をKotlinらしく記述できます。

詳細

Fuel 2025年版はKotlinマルチプラットフォーム対応のHTTPクライアントライブラリとして地位を確立し、バージョン3.0.0-alpha04で活発に開発が続けられています。各プラットフォームで最適化されたHTTPクライアント実装を自動選択(JVMではOkHttpClient、AppleプラットフォームではNSURLSession、WASMではFetch API)し、プラットフォーム間の差異を吸収。String拡張関数、Fuelオブジェクト、FuelBuilderの3つのアプローチで、シンプルなワンオフリクエストから複雑な設定を要する企業レベルのHTTP通信まで対応できます。

主な特徴

  • マルチプラットフォーム対応: JVM、Android、Apple、WASMで統一API
  • コルーチンネイティブ: Kotlinx Coroutinesによる完全非同期サポート
  • 柔軟なAPI設計: String拡張からFuelBuilderまで段階的な複雑さ対応
  • 自動プラットフォーム最適化: 各環境で最適なHTTPクライアント自動選択
  • 豊富なシリアライゼーション: Jackson、Moshi、Kotlinx Serialization対応
  • APIルーティング: FuelRoutingインターフェースによる構造化API呼び出し

メリット・デメリット

メリット

  • Kotlinマルチプラットフォームでの統一されたHTTP通信体験
  • String拡張関数による極めてシンプルで直感的なAPI記述
  • Kotlinx Coroutinesネイティブサポートによる自然な非同期プログラミング
  • プラットフォーム固有の最適化済みHTTPクライアント自動選択
  • 小規模プロジェクトから企業レベルまで対応する段階的複雑さ設計
  • Kotlinエコシステムとの優れた統合性とイディオマティックなコード

デメリット

  • バージョン3.xがアルファ版段階で本番利用には注意が必要
  • JVM専用シリアライゼーションモジュール(Jackson、Moshi)の制約
  • 完全マルチプラットフォーム環境ではKtorと比較してエコシステムが限定的
  • Java 8バイトコード要件による古い環境での制約
  • 新しいライブラリのためコミュニティリソースがRequestsほど豊富でない
  • プラットフォーム間の細かい動作差異の可能性

参考ページ

書き方の例

インストールと基本セットアップ

// build.gradle.kts(共通Kotlin)
dependencies {
    implementation("com.github.kittinunf.fuel:fuel:3.0.0-alpha04")
    implementation("com.github.kittinunf.fuel:fuel-coroutines:3.0.0-alpha04")
    
    // シリアライゼーション(必要に応じて)
    implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:3.0.0-alpha04")
    implementation("com.github.kittinunf.fuel:fuel-jackson:3.0.0-alpha04") // JVMのみ
    implementation("com.github.kittinunf.fuel:fuel-moshi:3.0.0-alpha04") // JVMのみ
}

// build.gradle(Android)
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

基本的なHTTPリクエスト(GET/POST/PUT/DELETE)

import fuel.*
import kotlinx.coroutines.*

// 最もシンプルなGETリクエスト(String拡張)
suspend fun simpleGet() {
    val response: String = "https://api.example.com/users".httpGet().body.string()
    println(response)
}

// Fuelオブジェクトを使用したGETリクエスト
suspend fun fuelObjectGet() {
    val response = Fuel.get("https://api.example.com/users")
    val stringData = response.body.string()
    println(stringData)
}

// パラメータ付きGETリクエスト
suspend fun getWithParameters() {
    val params = listOf(
        "page" to "1",
        "limit" to "10",
        "sort" to "created_at"
    )
    
    val response = Fuel.get("https://api.example.com/users", params).body.string()
    println(response)
}

// POSTリクエスト(JSON送信)
suspend fun postJson() {
    val jsonData = """
        {
            "name": "田中太郎",
            "email": "[email protected]",
            "age": 30
        }
    """
    
    val response = Fuel.post("https://api.example.com/users")
        .header("Content-Type" to "application/json")
        .header("Authorization" to "Bearer your-token")
        .body(jsonData)
        .body.string()
    
    println("User created: $response")
}

// POSTリクエスト(フォームデータ)
suspend fun postForm() {
    val formData = listOf(
        "username" to "testuser",
        "password" to "secret123"
    )
    
    val response = "https://api.example.com/login".httpPost(formData).body.string()
    println(response)
}

// PUTリクエスト(データ更新)
suspend fun putRequest() {
    val updateData = """
        {
            "name": "田中次郎",
            "email": "[email protected]"
        }
    """
    
    val response = Fuel.put("https://api.example.com/users/123")
        .header("Content-Type" to "application/json")
        .header("Authorization" to "Bearer your-token")
        .body(updateData)
        .body.string()
    
    println("User updated: $response")
}

// DELETEリクエスト
suspend fun deleteRequest() {
    val response = Fuel.delete("https://api.example.com/users/123")
        .header("Authorization" to "Bearer your-token")
    
    if (response.statusCode == 204) {
        println("User deleted successfully")
    }
}

// 使用例
fun main() = runBlocking {
    simpleGet()
    fuelObjectGet()
    getWithParameters()
    postJson()
    postForm()
    putRequest()
    deleteRequest()
}

高度な設定とカスタマイズ(認証、タイムアウト、プラットフォーム別設定)

import fuel.*
import kotlinx.coroutines.*

// FuelBuilderを使用したカスタム設定(JVM)
fun createCustomFuelJVM(): Fuel {
    return FuelBuilder()
        .config(
            okhttp3.OkHttpClient.Builder()
                .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                .build()
        )
        .build()
}

// FuelBuilderを使用したカスタム設定(Apple)
// expect/actual パターンでプラットフォーム別実装
expect fun createCustomFuelApple(): Fuel

// Apple実装(iOS/macOS)
actual fun createCustomFuelApple(): Fuel {
    val config = platform.Foundation.NSURLSessionConfiguration.defaultSessionConfiguration
    config.timeoutIntervalForRequest = 30.0
    config.timeoutIntervalForResource = 60.0
    
    return FuelBuilder()
        .config(config)
        .build()
}

// 認証付きリクエスト
suspend fun authenticatedRequest() {
    val headers = mapOf(
        "Authorization" to "Bearer your-jwt-token",
        "User-Agent" to "MyKotlinApp/1.0",
        "Accept" to "application/json",
        "X-API-Version" to "v2"
    )
    
    val response = Fuel.get("https://api.example.com/protected")
        .header(headers)
        .body.string()
    
    println(response)
}

// Basic認証
suspend fun basicAuth() {
    val credentials = "username:password"
    val encodedCredentials = credentials.encodeToByteArray().toBase64()
    
    val response = Fuel.get("https://api.example.com/private")
        .header("Authorization" to "Basic $encodedCredentials")
        .body.string()
    
    println(response)
}

// カスタムヘッダーとCookie
suspend fun customHeadersAndCookies() {
    val customHeaders = mapOf(
        "X-Custom-Header" to "custom-value",
        "Accept-Language" to "ja-JP,en-US",
        "Cache-Control" to "no-cache",
        "Cookie" to "session_id=abc123; user_pref=dark_mode"
    )
    
    val response = Fuel.get("https://api.example.com/user-data")
        .header(customHeaders)
        .body.string()
    
    println(response)
}

// レスポンス詳細情報の取得
suspend fun responseDetails() {
    val response = Fuel.get("https://api.example.com/info")
    
    println("Status Code: ${response.statusCode}")
    println("Headers: ${response.headers}")
    println("Content Type: ${response.headers["Content-Type"]}")
    println("Content Length: ${response.headers["Content-Length"]}")
    println("Body: ${response.body.string()}")
}

エラーハンドリングとリトライ機能

import fuel.*
import kotlinx.coroutines.*

// 包括的なエラーハンドリング
suspend fun safeRequest(url: String): String? {
    return try {
        val response = Fuel.get(url)
        
        when (response.statusCode) {
            in 200..299 -> response.body.string()
            401 -> {
                println("認証エラー: トークンを確認してください")
                null
            }
            403 -> {
                println("権限エラー: アクセス権限がありません")
                null
            }
            404 -> {
                println("見つかりません: リソースが存在しません")
                null
            }
            429 -> {
                println("レート制限: しばらく待ってから再試行してください")
                null
            }
            in 500..599 -> {
                println("サーバーエラー: ${response.statusCode}")
                null
            }
            else -> {
                println("予期しないステータス: ${response.statusCode}")
                null
            }
        }
    } catch (e: Exception) {
        println("リクエストエラー: ${e.message}")
        null
    }
}

// リトライ機能付きリクエスト
suspend fun requestWithRetry(
    url: String,
    maxRetries: Int = 3,
    backoffFactor: Long = 1000
): String? {
    repeat(maxRetries) { attempt ->
        try {
            val response = Fuel.get(url)
            if (response.statusCode in 200..299) {
                return response.body.string()
            }
            
            // 一時的なエラーの場合はリトライ
            if (response.statusCode in listOf(429, 500, 502, 503, 504)) {
                if (attempt < maxRetries - 1) {
                    val waitTime = backoffFactor * (1L shl attempt) // 指数バックオフ
                    println("試行 ${attempt + 1} 失敗 (${response.statusCode}). ${waitTime}ms後に再試行...")
                    delay(waitTime)
                    return@repeat
                }
            }
            
            println("最終的に失敗: ${response.statusCode}")
            return null
            
        } catch (e: Exception) {
            if (attempt < maxRetries - 1) {
                val waitTime = backoffFactor * (1L shl attempt)
                println("試行 ${attempt + 1} でエラー: ${e.message}. ${waitTime}ms後に再試行...")
                delay(waitTime)
            } else {
                println("最大試行回数に達しました: ${e.message}")
                return null
            }
        }
    }
    return null
}

// タイムアウト処理
suspend fun requestWithTimeout(url: String, timeoutMs: Long = 10000): String? {
    return try {
        withTimeout(timeoutMs) {
            Fuel.get(url).body.string()
        }
    } catch (e: TimeoutCancellationException) {
        println("リクエストがタイムアウトしました: ${timeoutMs}ms")
        null
    } catch (e: Exception) {
        println("リクエストエラー: ${e.message}")
        null
    }
}

// 使用例
suspend fun errorHandlingExamples() {
    // 安全なリクエスト
    val data1 = safeRequest("https://api.example.com/data")
    data1?.let { println("取得成功: $it") }
    
    // リトライ付きリクエスト
    val data2 = requestWithRetry("https://api.example.com/unstable")
    data2?.let { println("リトライ成功: $it") }
    
    // タイムアウト付きリクエスト
    val data3 = requestWithTimeout("https://api.example.com/slow", 5000)
    data3?.let { println("タイムアウト内成功: $it") }
}

並行処理とマルチプラットフォーム対応

import fuel.*
import kotlinx.coroutines.*

// 複数URLの並列取得
suspend fun parallelRequests() {
    val urls = listOf(
        "https://api.example.com/users",
        "https://api.example.com/posts", 
        "https://api.example.com/comments",
        "https://api.example.com/categories"
    )
    
    val results = urls.map { url ->
        async {
            try {
                val response = Fuel.get(url)
                url to response.body.string()
            } catch (e: Exception) {
                url to "Error: ${e.message}"
            }
        }
    }.awaitAll()
    
    results.forEach { (url, result) ->
        println("$url: ${result.take(100)}...")
    }
}

// ページネーション対応の全データ取得
suspend fun fetchAllPages(baseUrl: String, authToken: String): List<String> {
    val allData = mutableListOf<String>()
    var page = 1
    var hasMore = true
    
    while (hasMore) {
        try {
            val params = listOf("page" to page.toString(), "per_page" to "100")
            val response = Fuel.get(baseUrl, params)
                .header("Authorization" to "Bearer $authToken")
            
            if (response.statusCode == 200) {
                val data = response.body.string()
                allData.add(data)
                
                // 次ページの存在確認(レスポンスヘッダーまたはボディで判断)
                hasMore = response.headers["X-Has-More"]?.firstOrNull() == "true" ||
                         data.contains("\"has_more\":true")
                
                page++
                println("ページ $page 取得完了")
                
                // API負荷軽減
                delay(100)
            } else {
                println("エラー: ${response.statusCode}")
                break
            }
        } catch (e: Exception) {
            println("ページ $page でエラー: ${e.message}")
            break
        }
    }
    
    println("総取得ページ数: ${allData.size}")
    return allData
}

// プラットフォーム固有機能の利用
// Common
expect class PlatformHttpClient {
    suspend fun makeRequest(url: String): String
}

// JVM実装
actual class PlatformHttpClient {
    private val fuel = FuelBuilder()
        .config(
            okhttp3.OkHttpClient.Builder()
                .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                .addInterceptor { chain ->
                    val request = chain.request().newBuilder()
                        .addHeader("User-Agent", "MyApp-JVM/1.0")
                        .build()
                    chain.proceed(request)
                }
                .build()
        )
        .build()
    
    actual suspend fun makeRequest(url: String): String {
        return fuel.get(url).body.string()
    }
}

// Apple実装
actual class PlatformHttpClient {
    private val fuel = FuelBuilder()
        .config(
            platform.Foundation.NSURLSessionConfiguration.defaultSessionConfiguration.apply {
                timeoutIntervalForRequest = 30.0
                HTTPAdditionalHeaders = mapOf(
                    "User-Agent" to "MyApp-iOS/1.0"
                )
            }
        )
        .build()
    
    actual suspend fun makeRequest(url: String): String {
        return fuel.get(url).body.string()
    }
}

// キャッシュ機能付きHTTPクライアント
class CachedHttpClient {
    private val cache = mutableMapOf<String, Pair<String, Long>>()
    private val cacheTimeout = 5 * 60 * 1000L // 5分

    suspend fun get(url: String, useCache: Boolean = true): String {
        if (useCache) {
            cache[url]?.let { (cachedData, timestamp) ->
                if (System.currentTimeMillis() - timestamp < cacheTimeout) {
                    println("キャッシュヒット: $url")
                    return cachedData
                }
            }
        }
        
        val response = Fuel.get(url).body.string()
        cache[url] = response to System.currentTimeMillis()
        return response
    }
    
    fun clearCache() {
        cache.clear()
    }
}

// 使用例
suspend fun concurrencyExamples() {
    // 並列リクエスト実行
    parallelRequests()
    
    // 全ページ取得
    val allData = fetchAllPages("https://api.example.com/posts", "your-token")
    
    // プラットフォーム固有クライアント
    val platformClient = PlatformHttpClient()
    val result = platformClient.makeRequest("https://api.example.com/platform-info")
    
    // キャッシュ付きクライアント
    val cachedClient = CachedHttpClient()
    val data1 = cachedClient.get("https://api.example.com/slow-data") // ネットワーク
    val data2 = cachedClient.get("https://api.example.com/slow-data") // キャッシュ
}

API設計パターンとシリアライゼーション統合

import fuel.*
import kotlinx.coroutines.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// データクラス定義
@Serializable
data class User(
    val id: Int,
    val name: String,
    val email: String,
    val age: Int
)

@Serializable
data class ApiResponse<T>(
    val success: Boolean,
    val data: T? = null,
    val message: String? = null
)

// FuelRoutingを使用したAPI設計
sealed class UserAPI : FuelRouting {
    override val basePath = "https://api.example.com/v1"
    
    object GetUsers : UserAPI() {
        override val method = "GET"
        override val path = "users"
        override val parameters: List<Pair<String, String>>? = null
        override val headers: Map<String, String> = mapOf(
            "Accept" to "application/json"
        )
        override val body: String? = null
    }
    
    data class GetUser(val userId: Int) : UserAPI() {
        override val method = "GET"
        override val path = "users/$userId"
        override val parameters: List<Pair<String, String>>? = null
        override val headers: Map<String, String> = mapOf(
            "Accept" to "application/json"
        )
        override val body: String? = null
    }
    
    data class CreateUser(val user: User) : UserAPI() {
        override val method = "POST"
        override val path = "users"
        override val parameters: List<Pair<String, String>>? = null
        override val headers: Map<String, String> = mapOf(
            "Content-Type" to "application/json",
            "Accept" to "application/json"
        )
        override val body: String = Json.encodeToString(user)
    }
    
    data class UpdateUser(val userId: Int, val user: User) : UserAPI() {
        override val method = "PUT"
        override val path = "users/$userId"
        override val parameters: List<Pair<String, String>>? = null
        override val headers: Map<String, String> = mapOf(
            "Content-Type" to "application/json",
            "Accept" to "application/json"
        )
        override val body: String = Json.encodeToString(user)
    }
    
    data class DeleteUser(val userId: Int) : UserAPI() {
        override val method = "DELETE"
        override val path = "users/$userId"
        override val parameters: List<Pair<String, String>>? = null
        override val headers: Map<String, String> = mapOf(
            "Accept" to "application/json"
        )
        override val body: String? = null
    }
}

// APIクライアントクラス
class UserApiClient(private val authToken: String) {
    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true
    }
    
    suspend fun getUsers(): List<User>? {
        return try {
            val response = Fuel.request(UserAPI.GetUsers)
                .header("Authorization" to "Bearer $authToken")
            
            if (response.statusCode == 200) {
                val responseData = json.decodeFromString<ApiResponse<List<User>>>(
                    response.body.string()
                )
                responseData.data
            } else {
                println("エラー: ${response.statusCode}")
                null
            }
        } catch (e: Exception) {
            println("ユーザー一覧取得エラー: ${e.message}")
            null
        }
    }
    
    suspend fun getUser(userId: Int): User? {
        return try {
            val response = Fuel.request(UserAPI.GetUser(userId))
                .header("Authorization" to "Bearer $authToken")
            
            if (response.statusCode == 200) {
                val responseData = json.decodeFromString<ApiResponse<User>>(
                    response.body.string()
                )
                responseData.data
            } else {
                println("エラー: ${response.statusCode}")
                null
            }
        } catch (e: Exception) {
            println("ユーザー取得エラー: ${e.message}")
            null
        }
    }
    
    suspend fun createUser(user: User): User? {
        return try {
            val response = Fuel.request(UserAPI.CreateUser(user))
                .header("Authorization" to "Bearer $authToken")
            
            if (response.statusCode == 201) {
                val responseData = json.decodeFromString<ApiResponse<User>>(
                    response.body.string()
                )
                responseData.data
            } else {
                println("エラー: ${response.statusCode}")
                null
            }
        } catch (e: Exception) {
            println("ユーザー作成エラー: ${e.message}")
            null
        }
    }
    
    suspend fun updateUser(userId: Int, user: User): User? {
        return try {
            val response = Fuel.request(UserAPI.UpdateUser(userId, user))
                .header("Authorization" to "Bearer $authToken")
            
            if (response.statusCode == 200) {
                val responseData = json.decodeFromString<ApiResponse<User>>(
                    response.body.string()
                )
                responseData.data
            } else {
                println("エラー: ${response.statusCode}")
                null
            }
        } catch (e: Exception) {
            println("ユーザー更新エラー: ${e.message}")
            null
        }
    }
    
    suspend fun deleteUser(userId: Int): Boolean {
        return try {
            val response = Fuel.request(UserAPI.DeleteUser(userId))
                .header("Authorization" to "Bearer $authToken")
            
            response.statusCode == 204
        } catch (e: Exception) {
            println("ユーザー削除エラー: ${e.message}")
            false
        }
    }
}

// ファイルアップロード
suspend fun uploadFile(filePath: String, uploadUrl: String, authToken: String): Boolean {
    return try {
        // マルチプラットフォーム対応のファイル読み込み
        val fileBytes = readFileBytes(filePath) // プラットフォーム固有実装
        
        val response = Fuel.post(uploadUrl)
            .header("Authorization" to "Bearer $authToken")
            .header("Content-Type" to "multipart/form-data")
            .body(fileBytes)
        
        response.statusCode in 200..299
    } catch (e: Exception) {
        println("ファイルアップロードエラー: ${e.message}")
        false
    }
}

// プラットフォーム固有のファイル読み込み(expect/actual)
expect suspend fun readFileBytes(filePath: String): ByteArray

// 使用例
suspend fun apiPatternExamples() {
    val apiClient = UserApiClient("your-auth-token")
    
    // ユーザー一覧取得
    val users = apiClient.getUsers()
    users?.forEach { user ->
        println("User: ${user.name} (${user.email})")
    }
    
    // 新しいユーザー作成
    val newUser = User(
        id = 0,
        name = "田中太郎",
        email = "[email protected]", 
        age = 30
    )
    
    val createdUser = apiClient.createUser(newUser)
    createdUser?.let { user ->
        println("作成されたユーザー: ${user.name} (ID: ${user.id})")
        
        // ユーザー更新
        val updatedUser = user.copy(name = "田中次郎")
        apiClient.updateUser(user.id, updatedUser)
        
        // ユーザー削除
        val deleted = apiClient.deleteUser(user.id)
        if (deleted) {
            println("ユーザーが削除されました")
        }
    }
}