はじめに
Androidアプリ開発をしていると、必ずと言っていいほど非同期処理に出会いますよね。ネットワーク通信、データベースアクセス、ファイルI/Oなど、UIスレッドをブロックしてはいけない処理が山ほどあります。
従来のCallback地獄やRxJavaの複雑な記述に悩まされた経験がある方も多いのではないでしょうか。そんな悩みを解決してくれるのがKotlin Coroutinesです。
2025年現在、Kotlin Coroutines 1.10.2が最新安定版として提供されており、Androidアプリケーションのパフォーマンスとユーザー体験を大幅に改善できる技術として注目を集めています。
本記事では、Android開発におけるKotlin Coroutinesの基本的な使い方から実践的な活用方法、そして最新のベストプラクティスまでを、できるだけわかりやすく解説していきます。
そもそもKotlin Coroutinesって何?
基本的な考え方
Coroutineは日本語で「協調的なマルチタスク」という意味で、簡単に言うと軽量なスレッドのような存在です。
通常のスレッドと何が違うかというと、コルーチンは特定のスレッドに縛られることなく、実行を一時停止(suspend)して別のスレッドで再開することができるんです。これがめちゃくちゃ便利で、メモリ効率もいいんですよね。
fun main() {
runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
}
このコードを実行すると、以下のような出力が得られます:
Hello,
World!
「Hello,」が先に出力されて、1秒後に「World!」が表示されるという流れですね。
覚えておきたい主要コンポーネント
1. Coroutine Builders(コルーチンビルダー)
launch
新しいコルーチンを起動してJobを返してくれます。結果を返さない、いわゆる「fire and forget」な処理に向いています。
val job = launch {
// バックグラウンド処理
performTask()
}
async
新しいコルーチンを起動してDeferred<T>を返します。結果を取得したい処理にはこちらを使いましょう。
val deferred = async {
fetchUserData()
}
val result = deferred.await()
2. Suspend Functions(サスペンド関数)
suspendキーワードを付けることで、コルーチン内でのみ実行可能な特別な関数を作ることができます。これがCoroutinesの核となる機能ですね。
suspend fun fetchDataFromNetwork(): ApiResponse {
return withContext(Dispatchers.IO) {
// ネットワーク通信処理
apiService.getData()
}
}
Android開発での実際の使い方
LifecycleScope
Activityでは、lifecycleScopeを使うとライフサイクルに連動したコルーチンが簡単に実行できます。これも自動的に適切なタイミングでキャンセルしてくれるので安心です。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// アクティブ状態でのみ実行される処理
viewModel.users.collect { users ->
updateUI(users)
}
}
}
}
}
ViewModelScope
AndroidのViewModelでコルーチンを使う時は、viewModelScopeが超便利です。ViewModelが破棄される時に、実行中のコルーチンも自動的にキャンセルしてくれるので、メモリリークの心配がありません。
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users
fun loadUsers() {
viewModelScope.launch {
try {
_users.value = userRepository.getUsers()
} catch (e: Exception) {
// エラーハンドリング
handleError(e)
}
}
}
}
実際のプロジェクトでよくあるパターン
パターン1: 順番にAPI呼び出しをする場合
認証が必要なAPIを叩く時によくあるパターンですね。まず認証トークンを取得して、そのトークンを使ってデータを取得する流れです:
class AuthViewModel : ViewModel() {
fun authenticateAndFetchData() {
viewModelScope.launch {
try {
// 1. 認証トークンを取得
val token = authRepository.getAuthToken()
Log.d("Auth", "Token received: $token")
// 2. トークンを使用してデータを取得
val userData = userRepository.getUserData(token)
updateUI(userData)
} catch (e: Exception) {
handleError(e)
}
}
}
}
パターン2: 複数のAPIを同時に叩きたい場合
複数のAPIを同時に呼び出して処理時間を短縮したい場合は、asyncを使って並列処理にしましょう:
suspend fun fetchUserProfile() = coroutineScope {
try {
val userInfoDeferred = async { userRepository.getUserInfo() }
val userSettingsDeferred = async { userRepository.getUserSettings() }
val userPreferencesDeferred = async { userRepository.getUserPreferences() }
val userInfo = userInfoDeferred.await()
val userSettings = userSettingsDeferred.await()
val userPreferences = userPreferencesDeferred.await()
UserProfile(userInfo, userSettings, userPreferences)
} catch (e: Exception) {
throw UserProfileException("Failed to fetch user profile", e)
}
}
これだけは押さえておきたいベストプラクティス
1. スコープは適切に選ぼう
// ❌ これはやめよう:GlobalScopeの使用
GlobalScope.launch {
// アプリが終了するまでずっと実行され続ける可能性がある
// テストも書きにくい
}
// ✅ これが正解:viewModelScopeを使用
viewModelScope.launch {
// ViewModelの生存期間と連動して自動的にキャンセルされる
// メモリリークの心配なし!
}
2. Dispatcherは外から注入できるようにしよう
テストのことを考えると、Dispatcherは固定で書かない方がいいです:
// ❌ これだとテストが大変:Dispatcherを直接指定
class UserRepository {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
// 実際のIOを使うので、テスト時に制御できない
apiService.getUsers()
}
}
// ✅ テストしやすい書き方:Dispatcherを外から渡せるように
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getUsers(): List<User> = withContext(ioDispatcher) {
// テスト時はTestDispatcherを注入できる
apiService.getUsers()
}
}
3. 例外処理はしっかりと
ネットワーク処理では例外が発生することが多いので、適切にハンドリングしましょう:
class DataViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
try {
val data = dataRepository.fetchData()
_data.value = Resource.Success(data)
} catch (e: NetworkException) {
_data.value = Resource.Error("ネットワークエラーが発生しました")
} catch (e: Exception) {
_data.value = Resource.Error("予期しないエラーが発生しました")
}
}
}
}
4. 長時間の処理ではキャンセレーションを意識しよう
重い処理をしている途中でユーザーが画面を離れた場合、無駄な処理を続けるのはもったいないですよね。isActiveフラグやensureActive()を使って、定期的にキャンセル状態をチェックしましょう。
suspend fun processLargeDataSet(data: List<Item>) {
data.forEach { item ->
// キャンセルをチェック
ensureActive()
processItem(item)
}
}
// または
suspend fun processWithCancellationCheck() {
while (isActive) {
// 処理を継続
val result = performOperation()
// キャンセルを再チェック
coroutineContext.ensureActive()
}
}
5. 構造化された並行性を活用しよう
複数のコルーチンを扱う時はcoroutineScopeでグループ化するのがおすすめです。どれか一つでも失敗したら全体をキャンセルしてくれるので、中途半端な状態を避けることができます。
suspend fun performComplexOperation() = coroutineScope {
val operation1 = async { performOperation1() }
val operation2 = async { performOperation2() }
try {
val result1 = operation1.await()
val result2 = operation2.await()
combineResults(result1, result2)
} catch (e: Exception) {
// 一つの処理が失敗した場合、全てがキャンセルされる
throw e
}
}
よくやりがちな間違いと対処法
間違い1: メインスレッドをブロックしてしまう
これは本当によくある間違いです。避けましょう:
// ❌ これはダメ:UIがフリーズしちゃいます
class BadRepository {
fun getData(): List<Data> {
return runBlocking {
apiService.getData() // メインスレッドがブロックされてANRの原因に
}
}
}
// ✅ 正しい書き方:非同期で処理
class GoodRepository {
suspend fun getData(): List<Data> {
return withContext(Dispatchers.IO) {
// IOスレッドで実行されるのでUIがブロックされない
apiService.getData()
}
}
}
間違い2: launch と async の使い分けができていない
結果が欲しいかどうかで使い分けるのがポイントです:
// ❌ これだと結果を受け取れない
fun badAsyncOperation() {
val job = viewModelScope.launch {
val result = fetchData()
// resultをどこで使う?外に出せない...
}
}
// ✅ 結果が欲しいならasyncを使おう
fun goodAsyncOperation() {
viewModelScope.launch {
val deferred = async { fetchData() }
val result = deferred.await() // ここで結果を取得
handleResult(result) // 結果を使った処理ができる
}
}
まとめ
Kotlin Coroutines を使うと、Android アプリの非同期処理がめちゃくちゃ書きやすくなります。コードも読みやすくなるし、保守もしやすくなるので、まだ使ったことがない方はぜひ試してみてください!
今回は基本的な使い方を中心に説明しましたが、Coroutines は Flow という非同期データストリームの基盤技術でもあります。これらを組み合わせることで、もっと高度でリアクティブなアプリを作ることができるので、慣れてきたらそちらも挑戦してみてくださいね。