【正社員】還元率83%【フリーランス】マージン一律5万円で案件をご紹介させていただきます。 詳細はこちら

【Android】<Kotlin>Coroutinesとは?非同期処理の基本と実践

【Android】<Kotlin>Coroutinesとは?非同期処理の基本と実践
すだ

みなさまこんにちは〜!
メモリアインクのすだです。

本日は、
Androidアプリ開発における非同期処理の役割やKotlin Coroutinesの使い方についてわかりやすく解説していきます!

この記事を読んでわかること…
・非同期処理とは
・Kotlin Coroutinesの使い方

目次

環境

  • Kotlin (ver 1.9.0)
  • Android Studio (Giraffe | 2022.3.1 Patch 3)

非同期処理とは?なぜ必要?

非同期処理とは、「時間がかかる処理を、待たずに別で進める」仕組みです。
たとえば以下のような処理は、非同期で実行しないとアプリが固まってしまいます。

  • ネットワーク通信(APIの呼び出し)
  • データベース操作
  • 画像読み込みやファイル処理


Android アプリでは、UIを操作するメインスレッドが止まると「アプリが応答しない(ANR)」 という問題になります。(ユーザーからは、画面がフリーズしているように見えてしまう)
そのため、重い処理は必ず非同期で行う必要があるのです。

アプリ開発では「端末の画面(UI)」を直接制御しているため、非同期処理が“ユーザー体験”に直結します。

Kotlin Coroutinesとは

Coroutines(コルーチン) は、Kotlin が提供する非同期処理のための仕組みで、
「軽量スレッド(通常のスレッドよりもコストが小さく、効率的な小さな実行単位)」のように動作します。

非同期処理(Coroutines)とは、「メインスレッドを止めずに、重たい処理を裏側で実行する仕組み」のことです。
つまり、メインスレッドとバックグラウンドスレッドを“適切に切り替えながら” 並行処理を実現できる機能とも言えます。

スレッドの役割

メインスレッド(UIスレッド):画面の描画、ボタンの反応など、すべてのUI処理を担当。ここを止めてしまうとアプリが固まります。

バックグラウンドスレッド:ネット通信や重たい計算処理など、UIに関係ない処理を裏で担当。時間がかかる処理は基本的にこちらで行います。


Kotlin の Coroutines を使えば、これらのスレッドを意識しすぎることなく、簡単・安全に非同期処理を記述できます。

図解↓

【シングルスレッドで処理した場合】
UI ← 通信中...(画面フリーズ) ← データ取得後に再開

【Coroutineを使った非同期処理】
UI(メインスレッド)────▶ 表示処理続ける
          │
          └────▶ 通信処理(バックグラウンドスレッド)

➡ UIは止まらず、通信は裏で進む → 結果だけUIに戻す

Coroutinesの基本構文と用語

CoroutineScope

CoroutineScope とは、「コルーチンがどこで動くか・いつ終わるかを管理する“枠”のこと」です。
画面(Activity)や ViewModel にスコープを紐づけることで、画面が閉じられたら、その中の処理(コルーチン)も止める ことができます。

よく使う CoroutineScope の種類としては以下があります。

  • CoroutineScope(…)… 独自のスコープを作りたいとき
  • viewModelScope … ViewModelのライフサイクルに合わせて動く(UIが閉じたら処理も終了)
  • lifecycleScope … ActivityやFragmentのライフサイクルに合わせて動く(Jetpack)

suspend

suspend をつけることで、その関数は「途中で一時停止できる関数」になります。

一時停止(中断)とは:

  • 通信中に一旦止まって、終わったら続きから再開
  • 重たい処理を非同期で中断・再開できるようにする

ということを意味します。

suspend fun fetchUserName(): String {
    delay(1000) // ← コルーチンの「一時停止」
    return "Alice"
}

実用的なケース▼
たとえば、ファイルをダウンロードする suspend 関数があるとします。
このとき、ユーザーが「キャンセル」ボタンを押せば、そのダウンロード処理(=コルーチン)を途中でキャンセル(中断)することができます。これは、suspend 関数が中断可能であるからこそ実現できる仕組みです。

launchとasync / await

launchasync / awaitは、どちらもコルーチンビルダーと呼ばれるもので、
コルーチンをスタートさせるための命令です。

簡単に説明すると、
launch … 「何かを並行してやりたいけど、結果はいらない」という時に使う
async / await… 「非同期処理の結果が欲しい」という時に使う
という分け方です。

実際にコードを用いて比較してみます。

まず、非同期でユーザー名を取得する関数を用意します:

suspend fun fetchUserName(): String {
    delay(1000) // 通信に1秒かかるイメージ
    return "Alice"
}

launch だけを使う場合(結果を使わない)▼

viewModelScope.launch {
    fetchUserName() // 呼び出すだけ。結果は使わない
    println("処理完了!")
}

→結果をどこにも入れてないので、戻り値は無視されます。
これは例えば「ログ送信」「DBに保存」など、結果が不要な処理に向いています。

launch +async + await を使う場合(結果を使いたい)▼

viewModelScope.launch {
    val nameDeferred = async { fetchUserName() }
    val name = nameDeferred.await()
    println("取得したユーザー名: $name")
}

→こちらは、非同期で処理を開始(launch)し、後で結果を使う(await)パターンです。

  • async { ... }:非同期で処理開始(Deferred<String> を返す)
  • .await():その結果を待って取得

また、以下のように複数の処理を同時に走らせて、結果をまとめて取得できます▼

viewModelScope.launch {
    val userDeferred = async { fetchUserName() } // ① 非同期で開始
    val ageDeferred = async { fetchUserAge() }   // ② 同時にこっちも開始

    val name = userDeferred.await()              // ③ 結果を待つ
    val age = ageDeferred.await()                // ④ 結果を待つ

    println("名前: $name, 年齢: $age")
}

※ awaitは「中断」するだけなので、メインスレッドは止まりません・

Dispatchers

Dispatchers「この処理をどのスレッドで実行するか」を決めるものです。

基本的には launchwithContext() の中に指定して書きます。

launch に Dispatcher を指定するパターン:
そのブロック全体を指定したスレッドで動かす場合です▼

viewModelScope.launch(Dispatchers.IO) {
    val data = fetchData()
    println(data)
}

withContext を使ってスレッドを切り替えるパターン:
処理の一部だけを別スレッドに切り替える場合です▼

viewModelScope.launch {
    val data = withContext(Dispatchers.IO) {
        fetchData() // 重たい処理だけIOでやる
    }
    updateUI(data) // ← Mainスレッド(元に戻る)
}

よく使う Dispatcher の種類

Dispatchers.MainUIスレッド画面の更新、UIの操作
Dispatchers.IO入出力(通信・DB)ネットワーク通信、DB、ファイル操作など
Dispatchers.Default計算・CPU重い処理データ加工、リストのソートなど

実践形式で非同期処理を学ぶ

① 以下は キャンセル可能なファイルダウンロード処理を、Coroutinesを使って実現する例です。

import kotlinx.coroutines.*

var isDownloaded = false

suspend fun downloadJob(): Job {
    try {
        delay(7_000L)
    } catch (e: CancellationException) {
        println("ダウンロードがキャンセルされました")
    }
}

fun main() = runBlocking {
    println("ダウンロードスタート")
        
    val job = launch {
        isDownloaded = downloadFile()
    }
        
    delay(1_000L)
    job.cancelAndJoin()
        
    if(isDownloaded) {
        println(ダウンロード成功")
	} else {
        println(ダウンロード失敗")
	}
        
    println("ダウンロード状況: $isDownloaded")
}

このコードでは、suspend 関数として定義した中断可能な処理(downloadFile)を、launch ブロックの中で実行しています。
その後、cancelAndJoin() を使って 「そのコルーチンをキャンセルし、キャンセル処理の完了まで待機」する仕組みになっています。

  • cancel():コルーチンにキャンセルの合図を送る
  • join():コルーチンの完了を待つ
  • cancelAndJoin():この2つをまとめて行う便利な関数です

delay() はキャンセル可能な suspend 関数なので、
cancel() が呼ばれると CancellationException が発生し、catch ブロックに入ります。

すなわち、この処理の実行結果は以下のようになります。

ダウンロードスタート
ダウンロードがキャンセルされました
ダウンロード失敗
ダウンロード状況: false

② 次は ViewModel 内で Coroutines を使って、APIからデータを取得し、LiveData に流す例です。

class UserViewModel : ViewModel() {

    private val _user = MutableLiveData<String>()
    val user: LiveData<String> = _user

    private suspend fun fetchUserFromApi(): String {
        delay(1000) // 疑似API
        return "ユーザー名:山田太郎"
    }

    fun fetchUser() {
        viewModelScope.launch {
            try {
                val result = withContext(Dispatchers.IO) {
                    fetchUserFromApi()
                }
                _user.value = result
            } catch (e: Exception) {
                _user.value = "エラーが発生しました"
            }
        }
    }
}

このコードでも、suspend 関数として定義した中断可能な処理(fetchUserFromApi)を、launch ブロックの中で実行しています。
通信が走る処理なので、withContext(Dispatchers.IO)を使用しています。
非同期処理が完了したら、LiveData に値をセットする仕組みです。

まとめ

おつかれさまでした。いかがでしたでしょうか!

Android アプリ開発では、非同期処理をうまく扱えるかどうかが、アプリの品質に大きく関わってきます。
Coroutinesを使った非同期処理の書き方をぜひ覚えてください!

すだ

技術者としてのキャリアパスを次のレベルへと進めたい皆様、
未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】
を通じて、
自分の市場価値をさらに向上させてみませんか?

それではまた次回の記事でお会いしましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

弊社テックブログをご愛読いただきありがとうございます。
当テックブログを運用している株式会社メモリアインクは、
【正社員】還元率83%
【フリーランス】マージン一律5万円で案件のご紹介
と、エンジニアの皆様に分かりやすい形で稼げる仕組みを構築し提供させていただいております。

コメント

コメントする

目次