[Android] Kotlin Coroutinesの基本と、よくある落とし穴まとめ

スポンサーリンク

はじめに

Android開発でもはや避けて通れないのが Kotlin Coroutines
非同期処理をスッキリ書ける便利な機能ですが、使い方を間違えるとクラッシュやメモリリークの原因にもなります。
今回は、基本の使い方と、実務でハマりやすいポイントをセットでまとめてみました。

Coroutineの基本:launchとasync

まずは一番基本的な使い方から。
非同期処理を「並列に走らせたい」のか「順番に処理したい」のかで、使う関数が変わります。


GlobalScope.launch {
    // launchは結果を返さない
    delay(1000)
    println("launch完了!")
}

GlobalScope.async {
    // asyncは結果を返す
    delay(1000)
    "asyncの結果"
}.await()

launchは「火をつけて放置する」タイプ、
asyncは「結果を受け取る」タイプと覚えておくとイメージしやすいですね。

スコープの正しい使い方

実務ではGlobalScopeを使うのはおすすめしません。
ActivityやViewModelなど、ライフサイクルに応じてキャンセルできるスコープを使うのが基本です。


class MainViewModel : ViewModel() {

    fun fetchData() {
        viewModelScope.launch {
            val result = repository.loadData()
            Log.d("Coroutine", "結果: $result")
        }
    }
}

viewModelScopeを使うと、ViewModelが破棄されるタイミングでCoroutineもキャンセルされます。
これだけでリーク防止になります。

Dispatchersを理解する

スレッドを切り替えるには、Dispatchersを使います。


viewModelScope.launch(Dispatchers.IO) {
    // ネットワークやDBなど重い処理
    val data = api.getData()

    withContext(Dispatchers.Main) {
        // UIスレッドに戻って更新
        binding.textView.text = data
    }
}

このように、IO → Mainを意識的に切り替えるのがポイントです。
非同期処理でも「どのスレッドで動いているか」を意識しないとバグにつながります。

よくある落とし穴①:キャンセルされないCoroutine

例えばActivity内で以下のようにGlobalScopeを使うと、Activityが閉じても処理が走り続けます。


GlobalScope.launch {
    delay(5000)
    Log.d("Coroutine", "Activityはもうないけど動いてる…")
}

これは完全にアンチパターンです。
必ずlifecycleScopeviewModelScopeなど、
スコープをライフサイクルに紐づけて使いましょう。

よくある落とし穴②:Mainスレッドで重い処理

以下のように書いてしまうと、Mainスレッドでブロッキングが発生してしまいます。


lifecycleScope.launch {
    val result = heavyTask() // ← delayではなくThread.sleepを使うなど
    textView.text = result
}

内部処理が重い場合は、必ずwithContext(Dispatchers.IO)で切り替えましょう。


lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) { heavyTask() }
    textView.text = result
}

よくある落とし穴③:スコープの誤用

RecyclerView.AdapterなどでlifecycleScopeを直接呼ぶのも危険です。
Adapterはライフサイクルを持たないため、親FragmentやActivityからスコープを渡すようにしましょう。


class MyAdapter(private val scope: CoroutineScope) : RecyclerView.Adapter<ViewHolder>() {
    fun update() {
        scope.launch {
            // 安全に動作
        }
    }
}

こうしておけば、親のライフサイクルと連動して安全にキャンセルされます。

OS16でのCoroutine挙動について

Android 16(予定)では、バックグラウンド制限やスレッド優先度管理がより厳密になります。
そのため、重い処理をMainスレッドで走らせると、強制キャンセルされるケースもあります。
Coroutineを使う際は、今後さらにDispatchersの適切な指定が重要になります。

まとめ

  • GlobalScopeは使わない
  • スコープはライフサイクルに紐づける(viewModelScope / lifecycleScope)
  • 重い処理はDispatchers.IO、UI更新はDispatchers.Main
  • Android 16以降ではMainスレッド処理の制限がより強化される

Coroutineは「便利だけど危険な道具」。
正しくスコープを意識すれば、保守性もパフォーマンスも一気に上がります。
もしチームで使うなら、基本パターンを統一しておくのがおすすめです。

コメント

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