【Kotlin/Android】Result型の使い方!runCatchingとは?
この記事からわかること
- Android Studio/KotlinのResultの使い方
- onSuccess/onFailureの実装方法
- runCatchingとは?
index
[open]
\ アプリをリリースしました /
環境
- Android Studio:Narwhal Feature Drop
- Kotlin:2.0.21
- AGP:8.12.3
- gradle:8.13
Result型
inline class Result<out T> : Serializable
KotlinのResult型は操作の結果をラップして保持できるデータ型です。成功した際には指定したデータ型の値を失敗した際には例外を保持することができます。
結果を返すメソッドなどで返り値として使用されることが多く、成功時の失敗時のエラーハンドリングが行いやすくコードの見通しもスッキリさせることができます。
使い方
Result型をメソッドの返り値として指定する場合はResult.successで成功した場合のデータを、Result.failureで例外を返します。
private fun fetchLocalData(): Result <String> {
if (true) {
return Result.success("データ")
} else {
return Result.failure(Error("ERROR"))
}
}
onSuccess/onFailure
取得したResult型からはonSuccessで成功した場合の処理を、onFailureで失敗した場合の処理を記述することができます。
val result = fetchLocalData()
result
.onSuccess { data ->
println(data)
}.onFailure { error ->
println(error)
}
isSuccess/isFailure
isSuccessで成功しているかどうかを、isFailureで失敗しているかどうかを取得することができます。
result.isSuccess
result.isFailure
getOrNull/exceptionOrNull
getOrNullで値があれば取得なければnullを、exceptionOrNullでエラーがあれば取得なければnullを取得することができます。
val value = result.getOrNull()
val error = result.exceptionOrNull()
getOrDefault/getOrElse
getOrDefaultで値があれば取得なければ指定したデフォルト値を、getOrElseで値があれば取得なければデフォルト値をラムダ式で返却し取得することができます。
val value = result.getOrElse { "デフォルト値" }
val value = result.getOrDefault("デフォルト値")
getOrThrow
getOrThrowで値があれば取得、エラーなら例外をスローすることができます。
try {
// 成功している場合、値を取得
val value = successResult.getOrThrow()
println("Success: $value") // "Success: Hello, World!"
} catch (e: Exception) {
// 失敗している場合、例外をキャッチ
println("Error: ${e.message}")
}
fold
foldで成功時と失敗時の処理をそれぞれ指定し、その結果を返すことができます。
val msg = result.fold(
onSuccess = { value -> "Success: $value" },
onFailure = { error -> "Error: ${error.message}" }
)
recover
recoverで失敗時に指定した値を返すことができます。
val value = result.recover { "失敗した時に返したい値" }
runCatching
Result型を簡単に利用する方法にrunCatchingがあります。runCatchingメソッドは指定したラムダ式を実行しその結果をResult型で返します。ブロックが正常に完了した場合は成功としてResultにラップされ、例外が発生した場合は失敗としてResultにラップされます。
val successResult = runCatching {
"Hello, World!"
}
println(successResult) // Result.success(Hello, World!)
// 失敗する場合
val failureResult = runCatching {
throw Exception("Something went wrong")
}
println(failureResult) // Result.failure(java.lang.Exception: Something went wrong)
runCatchingとKotlin Coroutine
runCatchingはブロック内で発生した例外を補足しResult.failureにラップします。これはKotlin CoroutineのCancellationExceptionも捕捉してしまうようです。つまりスコープをキャンセルしたのにも関わらずrunCatchingに捕捉されてしまいスコープ自体が正常にキャンセルしない事象を招きます。
fun main() {
val screenScope = CoroutineScope(Dispatchers.IO)
println("Start")
screenScope.launch {
println("Launched")
val result = runCatching {
// 1秒停止
delay(1000)
4
}.getOrElse { 0 }
writeToDatabase(result)
}
// 0.5秒停止
Thread.sleep(500)
screenScope.cancel()
println("Job was cancelled: ${screenScope.coroutineContext[Job]?.isCancelled}")
println("Done")
}
suspend fun writeToDatabase(result: Int) {
println("Writing $result to database")
}
launch内の処理ではsleepで処理より先にスコープがキャンセルするように実装しています。このコードを実行すると以下のように出力されます。(※ コードはこちらから引用しています。)
Start
Launched
Writing 0 to database
Job was cancelled: true
Done
出力を見るとわかりますがキャンセルが発生しているのにwriteToDatabaseが実行されていることがわかります。これはrunCatchingがCancellationExceptionを捕捉してResultにラップしてしまうことでスコープまでキャンセルの例外が伝播しないためです。
これを防ぐにはsuspendRunCatchingのような独自の拡張関数を定義して回避する必要があります。正常にCancellationExceptionが流れるように捕捉したら再スローすることによってCancellationExceptionを閉じ込めないようにしています。
suspend fun <R> suspendRunCatching(block: suspend () -> R): Result<R> {
return try {
Result.success(block())
} catch(c: CancellationException) {
throw c
} catch (e: Throwable) {
Result.failure(e)
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。







