Android Retrofit HTTP client

在現代 Android 開發中,Retrofit 是由 Square 公司開發的最主流、最強大的網路請求函式庫。它本質上是一個 Type-safe 的 HTTP Client,能將 RESTful API 轉化為 Kotlin 介面 (Interface),並與 Coroutines 完美整合。

環境配置與依賴

除了 Retrofit 核心庫,我們通常還需要一個轉換器 (Converter) 來處理 JSON 解析,以及一個攔截器來輸出日誌。

build.gradle.kts 加入依賴:

dependencies {
    val retrofitVersion = "2.11.0"
    implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
    
    // 推薦使用 Kotlin Serialization 作為 JSON 轉換器
    implementation("com.squareup.retrofit2:converter-kotlinx-serialization:$retrofitVersion")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
    
    // OkHttp 攔截器 (用於 Log)
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
}

確保 AndroidManifest.xml 已加入網路權限:

<uses-permission android:name="android.permission.INTERNET" />

核心註解與請求方式

Retrofit 透過註解來描述 HTTP 請求的各個部分。

路徑與查詢參數 (@Path, @Query)

interface ApiService {
    // 取得單一使用者:/users/1
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: Int): User

    // 搜尋使用者:/users?sort=desc&page=1
    @GET("users")
    suspend fun searchUsers(
        @Query("sort") sortOrder: String,
        @Query("page") page: Int
    ): List<User>
}

Header 與 Body (@Header, @Body)

interface ApiService {
    // 動態傳入 Header
    @GET("profile")
    suspend fun getProfile(@Header("Authorization") token: String): Profile

    // 傳送 JSON Body
    @POST("users")
    suspend fun createUser(@Body user: User): User
}

表單與檔案上傳 (@FormUrlEncoded, @Multipart)

interface ApiService {
    // 模擬 HTML 表單提交
    @FormUrlEncoded
    @POST("login")
    suspend fun login(
        @Field("username") user: String,
        @Field("password") pass: String
    ): LoginResponse

    // 上傳檔案 (如圖片)
    @Multipart
    @POST("upload")
    suspend fun uploadAvatar(
        @Part avatar: MultipartBody.Part,
        @Part("description") desc: RequestBody
    ): UploadResponse
}

自定義 OkHttpClient 與攔截器

Retrofit 底層使用 OkHttp。透過配置 OkHttpClient,我們可以實現日誌輸出、超時設定以及自動注入內容 (如 Token)

身份驗證攔截器 (Auth Interceptor)

class AuthInterceptor(private val token: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(request)
    }
}

// 建立 Client
val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
    .addInterceptor(AuthInterceptor("your_token_here"))
    .build()

建立與 Hilt 整合

Hilt 中提供單一的 Retrofit 實例是最佳實踐。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        val json = Json { ignoreUnknownKeys = true } // 忽視未知欄位
        val contentType = "application/json".toMediaType()

        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(okHttpClient)
            .addConverterFactory(json.asConverterFactory(contentType))
            .build()
    }

    @Provides
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

優雅的錯誤處理

單純的 try-catch 很難區分「網路斷線」與「伺服器回傳 404」。我們可以使用自定義包裝類別:

sealed class NetworkResult<out T> {
    data class Success<out T>(val data: T) : NetworkResult<T>()
    data class Error(val code: Int, val message: String?) : NetworkResult<Nothing>()
    data class Exception(val e: Throwable) : NetworkResult<Nothing>()
}

// 在 Repository 中使用
suspend fun safeApiCall(): NetworkResult<List<User>> {
    return try {
        val response = apiService.getUsers()
        // Retrofit 配合協程會直接回傳內容,若需狀態碼可將回傳改為 Response<T>
        NetworkResult.success(response) 
    } catch (e: HttpException) {
        NetworkResult.Error(code = e.code(), message = e.message())
    } catch (e: Exception) {
        NetworkResult.Exception(e)
    }
}
如果需要精確的狀態碼,建議在 API Interface 定義時回傳 Response<T>,例如 suspend fun getUsers(): Response<List<User>>。這樣你就能手動判斷 response.isSuccessful 並讀取 response.code()

透過以上配置,你的網路層將不僅強大,而且具備良好的擴展性與維護性。