🛸

【Android】Ktor+HiltでAPIクライアント実装

2024/08/03に公開

概要

https://zenn.dev/slowhand/articles/d6f08410c6d698

前回の続きになります。今回はさらにHiltを使ってDIできるようにしたり、その他細かい部分の実装を進めていきます。

環境構築や準備

各バージョンや環境

# Android Studio Jellyfish | 2023.3.1
$ sw_vers
ProductName: macOS
ProductVersion: 14.5
BuildVersion: 23F79

プロジェクトは前回のものを継続して使う想定で進めていきます。

また、今回は👇の無料で使えるモック用のAPIを使います。

https://jsonplaceholder.typicode.com/todos/1

1. Hiltのインストール

1-1. gradle/libs.versions.toml に以下を追加

[versions]
# ※ kotlinのversionは "2.0.0"
hilt = "2.51.1"
ksp = "2.0.0-1.0.22"

[libraries]
# ktor
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

# hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }

[plugins]
# kotlin 2.0.0 対応
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

ksp-gradle-plugin = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt-android-gradle-plugin = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

今回は kotlin.serialization も使うため追加してます。

また io.ktor:ktor-client-content-negotiationio.ktor:ktor-serialization-kotlinx-json に関してはクライアントとサーバー間のメディア タイプのネゴシエーションと、JSON 形式での応答のシリアル化/逆シリアル化のために使用されます。

1-2. build.gradle.kts に以下を追加

plugins {
    alias(libs.plugins.jetbrains.kotlin.plugin.serialization) apply false
    alias(libs.plugins.ksp.gradle.plugin) apply false
    alias(libs.plugins.hilt.android.gradle.plugin) apply false
    alias(libs.plugins.compose.compiler) apply false
}

1-3. app/build.gradle.kts に以下を追加

plugins {
    alias(libs.plugins.jetbrains.kotlin.plugin.serialization)
    alias(libs.plugins.ksp.gradle.plugin)
    alias(libs.plugins.hilt.android.gradle.plugin)
    alias(libs.plugins.compose.compiler)
}

dependencies {
    // ktor
    implementation(libs.ktor.client.content.negotiation)
    implementation(libs.ktor.client.serialization.kotlinx.json)

    // hilt
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}

2. Hiltのセットアップ

1-1. Hiltアプリケーションクラスの作成

MainApplication.kt を以下内容で作成します。

@HiltAndroidApp
class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

AndroidManifest.xml に以下を追加します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        ....
        android:name=".MainApplication"  追加
        >

1-2. @AndroidEntryPoint の設定

MainActivity@AndroidEntryPoint を設定します。

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
  // ...

1-3. Hiltモジュールの作成

以下内容で ApiModule を作成します。

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Singleton
    @Provides
    fun provideHttpClient(): HttpClient {
        return HttpClient(OkHttp){
            install(DefaultRequest){
                url("https://jsonplaceholder.typicode.com")
                header(HttpHeaders.ContentType, ContentType.Application.Json)
            }
            install(ContentNegotiation){
                json(Json)
            }
            engine {
                addInterceptor(HttpLoggingInterceptor().apply {
                    level = HttpLoggingInterceptor.Level.BASIC
                })
            }
        }
    }
}

3. Serviceクラス実装

3-1. Resultクラス定義

API用のResultクラスを以下内容で作成しときます。

sealed class ApiResult<T>(val data: T? = null, val error: String? = null){
    class Success<T>(todo: T):ApiResult<T>(data = todo)
    class Error<T>(error: String):ApiResult<T>(error = error)
    class Loading<T>:ApiResult<T>()
}

3-2. モデル実装

以下内容で Todo.kt を作成します。

package com.example.ktorexample.data

@kotlinx.serialization.Serializable
data class Todo(
    val id: Int,
    val userId: Int,
    val title: String,
    val completed: Boolean
)

3-2. TodoServiceクラス実装

実際にリクエストする処理の TodoService.kt を以下内容で作成します。

class TodoService @Inject constructor(private val client: HttpClient) {
    suspend fun todo(id: Int): Flow<ApiResult<Todo>> = flow {
        emit(ApiResult.Loading())
        try {
            val response = client.get("/todos/$id")
            emit(ApiResult.Success(response.body()))
        } catch (e: Exception) {
            emit(ApiResult.Error(e.message ?: "Something went wrong"))
        }
    }
}

4. Compose部分に反映

最後に MainActivity.kt を以下内容に修正します。

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    @Inject
    lateinit var todoService: TodoService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            KtorExampleTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    val scope = rememberCoroutineScope()
                    var todo by remember { mutableStateOf<Todo?>(null) }
                    LaunchedEffect(true) {
                        scope.launch {
                            try {
                                todoService.todo(1).collect { todo = it.data }
                            } catch (e: Exception) {
                                e.localizedMessage ?: "error"
                            }
                        }
                    }
                    Todo(
                        todo = todo,
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Todo(todo: Todo?, modifier: Modifier = Modifier) {
    Text(
        text = "Todo: ${todo?.title}",
        modifier = modifier
    )
}

実際に起動してみると👇のようにレスポンスの title 部分が反映されていればOKです。

image1.png

参考URL

https://medium.com/@ominoblair/ktor-hilt-flow-in-android-bf116a630c3b

https://github.com/monjur35/Ktor-Client

Discussion