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関数 | 中断可能な関数 |
| CoroutineScope | Coroutineの生存範囲を管理 |
| 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 | 提供元 | ライフサイクル |
|---|---|---|
viewModelScope | ViewModel | ViewModelが破棄されるまで |
lifecycleScope | Activity/Fragment | Activity/Fragmentが破棄されるまで |
GlobalScope | Kotlin標準 | アプリが終了するまで(非推奨) |
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.Main | UI更新 | メインスレッド |
Dispatchers.IO | ネットワーク、DB、ファイル | バックグラウンド |
Dispatchers.Default | CPU負荷の高い計算 | バックグラウンド |
使い分けの例
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の違い
| 項目 | launch | async |
|---|---|---|
| 戻り値 | Job | Deferred<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内から呼ぶ |
| CoroutineScope | Coroutineの生存範囲、viewModelScopeを使う |
| Dispatcher | IO/Default/Mainを適切に使い分ける |
| launch/async | 結果が不要ならlaunch、必要ならasync |
| withContext | スレッドを切り替える |
最初は難しく感じるかもしれませんが、基本パターンを覚えれば、非同期処理がとても書きやすくなります。
// 基本パターン
viewModelScope.launch {
val data = withContext(Dispatchers.IO) {
repository.fetchData()
}
_uiState.value = data
}
このパターンを覚えておけば、ほとんどのケースに対応できます。
ぜひ実際のプロジェクトで使ってみてください!
次回は「Flow入門」として、Coroutinesと組み合わせてリアクティブな処理を書く方法を解説する予定です。

