1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android開発30日間マスターシリーズ - Day20: 非同期処理とCoroutines - UIをブロックしないバックグラウンド処理

Posted at

はじめに

アプリ開発では、ネットワーク通信やデータベースアクセスなど、時間がかかる処理がよくあります。これらの処理をそのまま実行すると、アプリがフリーズし、ユーザー体験を著しく損なう可能性があります。本日は、この問題を解決する非同期処理と、そのための最新の技術であるKotlin Coroutinesについて解説します。

1. なぜ非同期処理が必要なのか?

Androidアプリは、通常「メインスレッド(UIスレッド)」という1つのスレッドで動作しています。このスレッドは、UIの描画やユーザーからの入力(タップなど)の処理を担当しています。

もし、このメインスレッド上で時間のかかる処理(例: API通信)を実行してしまうと、その処理が終わるまでUIの更新が停止し、画面が固まってしまいます。これを「UIのブロック」と呼びます。

ANRとは?

Androidでは、メインスレッドが5秒以上ブロックされると「ANR(Application Not Responding)」が発生し、アプリが強制終了される可能性があります。これを避けるために非同期処理が必要不可欠です。

非同期処理は、時間のかかる処理をメインスレッドから切り離し、バックグラウンドスレッドで実行することで、UIをブロックせずにスムーズな操作を可能にするための技術です。

2. Coroutinesとは?

**Coroutines(コルーチン)**は、Kotlinが提供する軽量な非同期処理フレームワークです。スレッドよりもはるかに軽量な「コルーチン」という単位で処理を管理し、非同期処理をあたかも同期処理のように、読みやすいコードで書くことができます。

コルーチンの特徴

  • 軽量: スレッドに比べて起動コストが非常に低く、数千、数万のコルーチンを同時に実行してもメモリ消費が少ないです。
  • 構造化された並行処理: viewModelScopeのようなスコープを使うことで、コルーチンの実行をライフサイクルに合わせることができます。これにより、メモリリークのリスクを減らせます。
  • わかりやすい構文: suspendキーワードを使用することで、非同期処理を普通の関数呼び出しのように書けます。
  • キャンセル可能: 不要になったコルーチンは適切にキャンセルできるため、リソースの無駄遣いを防げます。

Dispatchersについて

コルーチンは、Dispatcherによって実行されるスレッドを制御できます:

  • Dispatchers.Main: UIスレッドで実行(UI更新用)
  • Dispatchers.IO: I/O操作(ネットワーク通信、ファイル操作)に最適化
  • Dispatchers.Default: CPU集約的なタスク(計算処理)に最適化
  • Dispatchers.Unconfined: 制限なし(通常は使用を避ける)

3. Coroutinesを使った非同期処理の実装

依存関係の追加

まず、build.gradle.kts(Module: app)に必要な依存関係を追加します:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
}

主要なコンポーネント

  • suspend 関数: 非同期処理が完了するまで「一時停止(suspend)」できる関数です。このキーワードは、コルーチン内からのみ呼び出せます。
  • CoroutineScope: コルーチンのライフサイクルを管理します。viewModelScopeはViewModelのライフサイクルに合わせてコルーチンを自動でキャンセルしてくれます。
  • launch ビルダー: 新しいコルーチンを起動します(戻り値なし)。
  • async ビルダー: 新しいコルーチンを起動し、結果をDeferredとして返します。

基本的な実装例

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class UserViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

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

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    fun fetchUser(userName: String) {
        // ① viewModelScopeでコルーチンを起動
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            
            try {
                // ② IOディスパッチャーでネットワーク通信を実行
                val fetchedUser = withContext(Dispatchers.IO) {
                    userRepository.getUser(userName)
                }
                
                // ③ メインスレッドでUI状態を更新
                _user.value = fetchedUser
            } catch (e: Exception) {
                // ④ エラーハンドリング
                _error.value = when (e) {
                    is java.net.UnknownHostException -> "インターネット接続を確認してください"
                    is java.net.SocketTimeoutException -> "通信がタイムアウトしました"
                    else -> "ユーザー情報の取得に失敗しました: ${e.message}"
                }
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun clearError() {
        _error.value = null
    }
}

複数の非同期処理を並行実行する例

import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll

fun loadUserData(userName: String) {
    viewModelScope.launch {
        _isLoading.value = true
        
        try {
            // 複数のAPIを並行して呼び出す
            val userDeferred = async(Dispatchers.IO) { 
                userRepository.getUser(userName) 
            }
            val reposDeferred = async(Dispatchers.IO) { 
                userRepository.getUserRepositories(userName) 
            }
            val followersDeferred = async(Dispatchers.IO) { 
                userRepository.getUserFollowers(userName) 
            }

            // すべての結果を待機
            val user = userDeferred.await()
            val repos = reposDeferred.await()
            val followers = followersDeferred.await()

            // UI更新
            _user.value = user
            _repositories.value = repos
            _followers.value = followers

        } catch (e: Exception) {
            _error.value = "データの読み込みに失敗しました: ${e.message}"
        } finally {
            _isLoading.value = false
        }
    }
}

Repository層での実装

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class UserRepository(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    // suspend関数として定義
    suspend fun getUser(userName: String): User {
        return withContext(Dispatchers.IO) {
            try {
                // まずAPIから取得を試行
                val user = apiService.getUser(userName)
                // キャッシュに保存
                userDao.insertUser(user)
                user
            } catch (e: Exception) {
                // ネットワークエラーの場合はキャッシュから取得
                userDao.getUser(userName) ?: throw e
            }
        }
    }

    suspend fun getUserRepositories(userName: String): List<Repository> {
        return withContext(Dispatchers.IO) {
            apiService.getUserRepositories(userName)
        }
    }
}

4. コルーチンの実行フロー解説

上記のコードの内部で何が起こっているか詳しく見てみましょう:

  1. viewModelScope.launch が呼び出されると、新しいコルーチンがバックグラウンドで開始されます。
  2. withContext(Dispatchers.IO) により、ネットワーク通信は適切なIOスレッドで実行されます。
  3. userRepository.getUser()suspend関数なので、この処理が完了するまでコルーチンは一時停止します。
  4. その間、メインスレッドはブロックされず、ユーザーはUIを操作できます。
  5. API通信が完了すると、コルーチンは一時停止を解除し、_user.valueを更新します。
  6. LiveDataが自動的にUIスレッドで値を更新してくれるため、安全にUIに反映されます。

5. コルーチンを使わない場合との比較

もしコルーチンを使わない場合、以下のような複雑なコールバック地獄に陥ってしまう可能性があります:

// Coroutinesを使わない場合の例(非推奨)
fun fetchUserOldWay(userName: String) {
    userRepository.getUserAsync(userName, object: Callback<User> {
        override fun onSuccess(user: User) {
            // さらにリポジトリ情報も取得したい場合...
            userRepository.getUserRepositoriesAsync(userName, object: Callback<List<Repository>> {
                override fun onSuccess(repos: List<Repository>) {
                    // さらにフォロワー情報も...
                    userRepository.getUserFollowersAsync(userName, object: Callback<List<User>> {
                        override fun onSuccess(followers: List<User>) {
                            // やっとUIを更新
                            runOnUiThread {
                                updateUI(user, repos, followers)
                            }
                        }
                        override fun onFailure(e: Exception) {
                            // エラーハンドリング
                        }
                    })
                }
                override fun onFailure(e: Exception) {
                    // エラーハンドリング
                }
            })
        }
        override fun onFailure(e: Exception) {
            // エラーハンドリング
        }
    })
}

Coroutinesを使うことで、このような複雑なコールバックチェーンを回避し、非同期処理の流れを上から下に順を追って書けるようになり、コードの可読性が飛躍的に向上します。

6. 注意点とベストプラクティス

メモリリークの防止

  • 必ず適切なCoroutineScopeを使用する(viewModelScope, lifecycleScopeなど)
  • 長時間実行されるコルーチンは適切にキャンセルする

エラーハンドリング

  • 必ずtry-catchブロックでエラーを捕捉する
  • ネットワークエラーやタイムアウトなど、具体的なエラーメッセージを提供する

パフォーマンス最適化

  • 適切なDispatcherを選択する
  • 不要なコルーチンの起動を避ける
  • 重い処理はDispatchers.Defaultで実行する

まとめ

  • 非同期処理は、UIをブロックせずに時間のかかる処理を行うために不可欠な技術です。
  • Kotlin Coroutinesは、非同期処理をシンプルかつ安全に実装するための強力なフレームワークです。
  • suspend関数をCoroutineScope.launchで実行し、適切なDispatcherを使用することで、UIスレッドをブロックすることなく、ネットワーク通信やデータベース操作を行えます。
  • 適切なエラーハンドリングとライフサイクル管理により、安定したアプリケーションを構築できます。

これらの知識は、LLMのようなAPI通信が主となるアプリ開発において、ユーザー体験を向上させるために最も重要な要素の一つです。次回は、これまでの知識を総動員して、いよいよLLMアプリの機能実装に取り掛かります。お楽しみに!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?