KMP+OpenAPIで始めるAPI開発:技術検証の第三歩(Result型の扱い方編)
はじめに
KMPの運用にあたり、KotlinのResultとSwiftのResultの相互変換をKmmResultというライブラリを使用してシームレスに行いました。
そこで、簡単にKmmResultについて紹介します。
KmmResultについて
KmmResultを使用することで、SwiftのResultとKotlinのResultを問題なく変換及び使用することができます。
KmmResultの具体的な使用コード例
技術検証で扱うPoCの中で、KmmResultをKMPのDomain層とApplication層で使用しています。
ドメイン層
interface HogeRepository {
suspend fun hogeGetHoge(): KmmResult<Hoge>
}
class HogeRepositoryImpl: HogeRepository {
private val hogeAPI: HogeAPI = HogeAPI()
override suspend fun hogeGetHoge(): KmmResult<Hoge> {
return try {
val result = hogeAPI.getHoge().toDomain()
KmmResult.success(result)
} catch (e: Exception) {
println(e)
KmmResult.failure(e)
}
}
}
アプリケーション層
class HogeGetUseCase: HogeGetUseCaseInterface {
private val repository: HogeRepository = HogeRepositoryImpl()
override suspend fun execute(): KmmResult<Hoge> = repository.hogeGetHoge()
}
各プラットフォームでのKmmResultの扱い方
上記のUseCaseをiOS及びAndroidから実際に呼び出してみます。
iOS
func getHoge() {
Task {
uiState.isLoading = true
do {
let result = try await hogeGetUseCase.execute().getOrThrow()
uiState.hogeInfo = result
} catch {
print(error)
uiState.error = error.localizedDescription
}
uiState.isLoading = false
}
}
.getOrThrow()でデータがnullである場合などに例外をスローするようにしています。
.getOrThrow()の具体的な実装内容は以下になります。
@InlineOnly
@SinceKotlin("1.3")
public inline fun <T> Result<T>.getOrThrow(): T {
throwOnFailure()
return value as T
}
また、.fold()を用いて成功時と失敗時のコールバック処理を明確に分ける方法もあります。
try await hogeGetUseCase.execute().fold(
onSuccess: { result in
self.uiState.hogeInfo = result
return result
},
onFailure: { error in
print(error)
self.uiState.error = error.message
return error
}
)
.fold()の具体的な実装内容は以下になります。
inline fun <R> fold(
onSuccess: (value: T) -> R,
onFailure: (exception: Throwable) -> R,
): R {
contract {
callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
}
return if (isSuccess) {
onSuccess(getOrThrow())
} else {
onFailure(exceptionOrNull()!!)
}
}
contractブロック: callsInPlaceで、onSuccessとonFailureが最大1回だけ呼ばれることをコンパイル時に保証しています。
成功時: getOrThrow()で取得した値を使って成功時の処理を実行。
失敗時: exceptionOrNull()で取得したエラーを非null型にキャストし、失敗時の処理を実行。
Android
fun getHoge() {
viewModelScope.launch {
val response = hogeGetUseCase.execute()
response.getOrNull()?.let { value ->
_state.value = _state.value.copy(
isLoading = false,
hogeInfo = value
)
} ?: run {
_state.value = _state.value.copy(
isLoading = false,
error = response.exceptionOrNull()?.message ?: "An unknown error"
)
}
}
}
.getOrNull()で取得し、スコープ関数であるletを用いてKmmResult型の処理を行っています。
Swiftと同じく、.fold()を用いて成功時と失敗時のコールバック処理を明確に分けることも出来ます。
fun getHoge() {
viewModelScope.launch {
val response = hogeGetUseCase.execute()
response.fold(
onSuccess = { value ->
_state.value = _state.value.copy(
isLoading = false,
hogeInfo = value
)
},
onFailure = { exception ->
_state.value = _state.value.copy(
isLoading = false,
error = exception.message ?: "An unknown error"
)
}
)
}
}
KotlinのResultをSwiftのResultにどのように置き換えているのか
結論:KmmResultクラスはKotlinのResult<T>を内部に保持し、SwiftからはこのKmmResultを介してKotlinのResultにアクセスします。つまり、KmmResultはSwiftとKotlinの間でデータを受け渡すためのラッパークラスの役割を果たしていると言えます。
class KmmResult<out T>
private constructor(
private val delegate: Result<T>,
@Suppress("UNUSED_PARAMETER") unusedButPreventsSignatureClashes: Boolean
) {
// ...
}
delegateは実際にはKotlinのResult<T>であり、Swiftからはこのdelegateを通じてResultの内容にアクセスします。
@class SharedKmmresultKmmResult<__covariant T>, ObjectA, ObjectB, ...;
また、SharedKmmResultKmmResult<__covariant T>の形式でObjective-Cインターフェースが生成され、SwiftからKotlinのKmmResultが呼び出されます。
SharedKmmResultKmmResultクラスは、Kotlinのジェネリクスを含んだ型をObjective-Cに変換し、Swiftコード内で利用可能にします。この時、ObjectAやObjectBなどの型パラメータは具体的な型として指定されます。
まとめ
KmmResultを使用して問題なくシームレスにSwiftとKotlinのResultを変換させることができました。
次回はKMPの主要なライブラリについて紹介してみようと思います!
プログリットの成長を加速させる仲間を募集しています!
プログリットでは、プロダクト開発のメンバーを募集しています!
「世界で自由に活躍できる人を増やす」というミッションに共感してくださる方、組織の中でお互いに切磋琢磨しながら成長していきたいという方は、ぜひカジュアル面談でお話しましょう!