【Kotlin】Coroutines入門|Androidの非同期処理をシンプルに書く方法

開発
Screenshot

Androidアプリ開発で避けて通れないのが「非同期処理」です。

APIからデータを取得する、データベースにアクセスする、ファイルを読み書きする。これらの処理をメインスレッドで実行すると、アプリがフリーズしてしまいます。

Kotlin Coroutinesを使えば、こうした非同期処理をシンプルかつ直感的に書けるようになります。

この記事では、Coroutinesの基本から実践的な使い方まで、初心者にもわかりやすく解説します。

なぜCoroutinesが必要なのか

メインスレッドをブロックしてはいけない

Androidアプリには「メインスレッド(UIスレッド)」があり、画面の描画やユーザー操作の処理を担当しています。

このメインスレッドで重い処理を実行すると、画面がフリーズして「アプリが応答していません(ANR)」というエラーが発生します。

// ❌ NG: メインスレッドで重い処理を実行
fun loadData() {
    val data = api.fetchData()  // 数秒かかる処理
    textView.text = data        // UIがフリーズする
}

従来の非同期処理の問題点

Coroutines以前は、AsyncTaskやCallback、RxJavaなどが使われていました。

// Callbackを使った従来の方法
api.fetchData(object : Callback<String> {
    override fun onSuccess(data: String) {
        // 成功時の処理
        api.fetchMoreData(data, object : Callback<String> {
            override fun onSuccess(moreData: String) {
                // さらにネストが深くなる...(コールバック地獄)
            }
            override fun onFailure(e: Exception) { }
        })
    }
    override fun onFailure(e: Exception) { }
})

これは「コールバック地獄」と呼ばれ、コードが読みにくくなる原因でした。

Coroutinesなら同期的に書ける

Coroutinesを使えば、非同期処理を同期的なコードのように書けます。

// ✅ OK: Coroutinesを使った方法
fun loadData() {
    viewModelScope.launch {
        val data = api.fetchData()          // 非同期だけど同期的に書ける
        val moreData = api.fetchMoreData(data)
        textView.text = moreData
    }
}

コードがシンプルで読みやすくなりました。

Coroutinesの基本概念

3つの重要な要素

要素役割
suspend関数中断可能な関数
CoroutineScopeCoroutineの生存範囲を管理
Dispatcherどのスレッドで実行するかを決める

順番に見ていきましょう。

1. suspend関数

suspendキーワードをつけた関数は、処理を「中断」して「再開」できる特別な関数になります。

// suspend関数の定義
suspend fun fetchUserData(): User {
    // 時間のかかる処理(API呼び出しなど)
    return api.getUser()
}

suspend関数のルール

// ❌ NG: 通常の関数からsuspend関数を呼べない
fun normalFunction() {
    val user = fetchUserData()  // コンパイルエラー
}

// ✅ OK: suspend関数からsuspend関数を呼べる
suspend fun anotherSuspendFunction() {
    val user = fetchUserData()  // OK
}

// ✅ OK: Coroutine内からsuspend関数を呼べる
fun startCoroutine() {
    viewModelScope.launch {
        val user = fetchUserData()  // OK
    }
}

2. CoroutineScope

CoroutineScopeは、Coroutineの「生存範囲」を管理します。

Scopeがキャンセルされると、その中で実行中のCoroutineもすべてキャンセルされます。これにより、メモリリークを防げます。

Android開発でよく使うScope

Scope提供元ライフサイクル
viewModelScopeViewModelViewModelが破棄されるまで
lifecycleScopeActivity/FragmentActivity/Fragmentが破棄されるまで
GlobalScopeKotlin標準アプリが終了するまで(非推奨)
class MyViewModel : ViewModel() {
    
    fun loadData() {
        // ViewModelが破棄されると自動でキャンセルされる
        viewModelScope.launch {
            val data = repository.fetchData()
            _uiState.value = data
        }
    }
}

GlobalScopeは使わない

// ❌ NG: GlobalScopeは使わない
GlobalScope.launch {
    // ライフサイクルと紐づかないので、メモリリークの原因になる
}

3. Dispatcher

Dispatcherは、Coroutineをどのスレッドで実行するかを決めます。

Dispatcher用途スレッド
Dispatchers.MainUI更新メインスレッド
Dispatchers.IOネットワーク、DB、ファイルバックグラウンド
Dispatchers.DefaultCPU負荷の高い計算バックグラウンド

使い分けの例

viewModelScope.launch {
    // デフォルトはDispatchers.Main
    
    // IOスレッドでデータを取得
    val data = withContext(Dispatchers.IO) {
        repository.fetchData()
    }
    
    // メインスレッドでUIを更新(自動で戻る)
    _uiState.value = data
}

Coroutineの起動方法

launch:結果を返さない

viewModelScope.launch {
    // 結果を返さない処理
    repository.saveData(data)
    showToast("保存しました")
}

async:結果を返す

viewModelScope.launch {
    // 並列で2つのAPIを呼び出す
    val userDeferred = async { api.fetchUser() }
    val postsDeferred = async { api.fetchPosts() }
    
    // 両方の結果を待つ
    val user = userDeferred.await()
    val posts = postsDeferred.await()
    
    // 両方揃ったら処理
    _uiState.value = UiState(user, posts)
}

launchとasyncの違い

項目launchasync
戻り値JobDeferred<T>
結果の取得なしawait()で取得
用途Fire and forget結果が必要な場合

実践:APIからデータを取得する

Repositoryパターンでの実装例

// Repository
class UserRepository(
    private val api: UserApi,
    private val userDao: UserDao
) {
    suspend fun getUser(id: String): User {
        return withContext(Dispatchers.IO) {
            // まずキャッシュを確認
            val cached = userDao.findById(id)
            if (cached != null) {
                return@withContext cached
            }
            
            // なければAPIから取得
            val user = api.fetchUser(id)
            userDao.insert(user)  // キャッシュに保存
            user
        }
    }
}

// ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                _user.value = repository.getUser(id)
            } catch (e: Exception) {
                // エラーハンドリング
            } finally {
                _isLoading.value = false
            }
        }
    }
}

エラーハンドリング

try-catchを使う

viewModelScope.launch {
    try {
        val data = repository.fetchData()
        _uiState.value = UiState.Success(data)
    } catch (e: IOException) {
        _uiState.value = UiState.Error("ネットワークエラー")
    } catch (e: Exception) {
        _uiState.value = UiState.Error("予期しないエラー")
    }
}

CoroutineExceptionHandlerを使う

private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    _uiState.value = UiState.Error(exception.message ?: "エラーが発生しました")
}

fun loadData() {
    viewModelScope.launch(exceptionHandler) {
        val data = repository.fetchData()
        _uiState.value = UiState.Success(data)
    }
}

よくある間違いと対処法

1. メインスレッドでの重い処理

// ❌ NG
viewModelScope.launch {
    val result = heavyCalculation()  // メインスレッドで実行される
}

// ✅ OK
viewModelScope.launch {
    val result = withContext(Dispatchers.Default) {
        heavyCalculation()  // バックグラウンドで実行
    }
}

2. suspend関数内でのスレッド指定忘れ

// ❌ NG: suspend関数内でもスレッド指定が必要
suspend fun fetchData(): Data {
    return api.getData()  // どのスレッドで実行されるか不明
}

// ✅ OK: 明示的にIOスレッドを指定
suspend fun fetchData(): Data {
    return withContext(Dispatchers.IO) {
        api.getData()
    }
}

3. Coroutineのキャンセルを考慮しない

// ❌ NG: キャンセルされても処理が続く
suspend fun downloadFile() {
    while (true) {
        // 無限ループはキャンセルされない
    }
}

// ✅ OK: isActiveでキャンセルをチェック
suspend fun downloadFile() {
    while (isActive) {  // キャンセルされたらループを抜ける
        // 処理
    }
}

// ✅ OK: ensureActive()を使う
suspend fun downloadFile() {
    repeat(100) {
        ensureActive()  // キャンセルされていたら例外をスロー
        // 処理
    }
}

Coroutinesを使うための設定

build.gradleに追加

dependencies {
    // Coroutines本体
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // viewModelScope用
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    
    // lifecycleScope用
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}

まとめ

Kotlin Coroutinesの基本をまとめると:

概念ポイント
suspend関数中断・再開可能な関数、Coroutine内から呼ぶ
CoroutineScopeCoroutineの生存範囲、viewModelScopeを使う
DispatcherIO/Default/Mainを適切に使い分ける
launch/async結果が不要ならlaunch、必要ならasync
withContextスレッドを切り替える

最初は難しく感じるかもしれませんが、基本パターンを覚えれば、非同期処理がとても書きやすくなります。

// 基本パターン
viewModelScope.launch {
    val data = withContext(Dispatchers.IO) {
        repository.fetchData()
    }
    _uiState.value = data
}

このパターンを覚えておけば、ほとんどのケースに対応できます。

ぜひ実際のプロジェクトで使ってみてください!


次回は「Flow入門」として、Coroutinesと組み合わせてリアクティブな処理を書く方法を解説する予定です。

タイトルとURLをコピーしました