はじめに
Kotlin では、非同期処理の標準として Coroutine + Flow が使われています。
しかし、ネットワークやDBアクセスなどの実行中に
「例外処理」や「Loading状態管理」が複雑になりがちです。
そこで登場するのが:
sealed Result × Flow × Coroutine の三位一体構成
これを使うと、
API呼び出しやデータ取得の全ライフサイクルを
型安全かつ宣言的 に扱うことができます。
Step 1. Result を sealed class で定義
まずは成功・失敗・読み込み状態を表す Result<T>。
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val exception: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
-
Success:処理成功 -
Failure:例外保持 -
Loading:実行中状態
Step 2. inline + reified で安全な例外ラップ
例外を安全に包む safeCall 関数を定義します。
inline fun <T> safeCall(block: () -> T): Result<T> {
return try {
Result.Success(block())
} catch (e: Exception) {
Result.Failure(e)
}
}
使用例
val result = safeCall { api.getUser() }
Step 3. Flow で非同期処理を統一化(safeApiFlow)
ここが本記事の核心です。
API呼び出しなどを Flow でラップし、
「Loading → Success / Failure」までの流れを自動化します。
import kotlinx.coroutines.flow.*
inline fun <T> safeApiFlow(
crossinline block: suspend () -> T
): Flow<Result<T>> = flow {
emit(Result.Loading)
try {
val data = block()
emit(Result.Success(data))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
使用例
val userFlow = safeApiFlow {
api.getUser() // suspend関数
}
userFlow.collect { result ->
when (result) {
is Result.Loading -> println("読み込み中...")
is Result.Success -> println("成功: ${result.data}")
is Result.Failure -> println("失敗: ${result.exception.message}")
}
}
これで「try-catch不要」かつ「状態管理をFlowで自動化」できます。
Step 4. reified 型でエラー型ごとの処理も可能に
reified を使えば、特定の例外型を安全にハンドリング可能です。
inline fun <reified E : Throwable> Result<*>.onErrorOfType(action: (E) -> Unit): Result<*> {
if (this is Result.Failure && exception is E) {
action(exception as E)
}
return this
}
使用例
safeApiFlow { api.getUser() }
.collect { result ->
result.onErrorOfType<HttpException> {
println("HTTPエラー: ${it.code()}")
}
}
Step 5. safeApiFlow の拡張: リトライ / タイムアウト対応
Flow のオペレータと組み合わせて、再試行や制限も容易です。
val resultFlow = safeApiFlow { api.getUser() }
.retry(3) { e -> e is IOException } // 3回まで再試行
.catch { e -> emit(Result.Failure(e)) }
.onCompletion { println("完了しました") }
→ Coroutine Flow の組み合わせで、
リトライ・再購読・キャンセル にも対応。
Step 6. ViewModel 層での利用(MVI 構成例)
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<Result<User>>(Result.Loading)
val uiState = _uiState.asStateFlow()
fun loadUser() {
viewModelScope.launch {
repository.getUserFlow().collect {
_uiState.value = it
}
}
}
}
class UserRepository(private val api: UserApi) {
fun getUserFlow(): Flow<Result<User>> = safeApiFlow {
api.getUser()
}
}
→ UI層では when(result) で状態を安全に切り替えるだけ。
viewModel.uiState.collect { result ->
when (result) {
is Result.Loading -> showLoading()
is Result.Success -> showUser(result.data)
is Result.Failure -> showError(result.exception.message)
}
}
拡張パターン: 汎用 ResultHandler
複数のAPI結果を統一処理するヘルパーも作れます。
inline fun <T> Result<T>.handle(
onLoading: () -> Unit = {},
onSuccess: (T) -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
when (this) {
is Result.Loading -> onLoading()
is Result.Success -> onSuccess(data)
is Result.Failure -> onError(exception)
}
}
使用例
safeApiFlow { api.getUser() }
.collect { result ->
result.handle(
onLoading = { println("ロード中...") },
onSuccess = { println("成功: $it") },
onError = { println("失敗: ${it.message}") }
)
}
まとめ
| 要素 | 役割 | 効果 |
|---|---|---|
sealed class Result |
成功・失敗・読み込みを型で表現 | 網羅性・安全性 |
inline |
パフォーマンス最適化・tryブロック内展開 | 高速 & 明確 |
reified |
例外型を実行時に識別 | 型安全なエラーハンドリング |
Flow |
非同期データストリーム | Coroutine連携・リアクティブ処理 |
safeApiFlow |
非同期処理の統一パターン | 再利用性・シンプル設計 |
終わりに
sealed Result × Flow × Coroutine の組み合わせは、
「try-catch地獄」から脱却し、
宣言的で安全な非同期アーキテクチャ を実現するための最強トリオです。
- 状態遷移を型で保証
- エラー処理を統一
- ViewModel と UI の接続を簡潔化