Contents
Gradle で Coroutines を安全に導入する基本設定
ポイント
Android アプリに Kotlin Coroutines を組み込む際は、公式が推奨している 安定版 のライブラリを implementation に追加し、structured concurrency(構造化並行性)を必ず意識したスコープで起動することが不可欠です。
1‑1. 現在の最新安定バージョンを使う
2024 年 11 月時点での Kotlin Coroutines の安定版は 1.7.3 です(※将来的なバージョン番号は記載しません)。build.gradle.kts に以下のように記述します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
plugins { id("com.android.application") kotlin("android") // Kotlin Android plugin kotlin("kapt") // KAPT が必要なら追加 } android { compileSdk = 34 // Android 14 (API 34) が最新の安定 SDK defaultConfig { applicationId = "com.example.coroutinesdemo" minSdk = 21 targetSdk = 34 versionCode = 1 versionName = "1.0" } } dependencies { // Coroutines core + Android‑specific dispatcher implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // テストで利用する場合は testImplementation に追加 testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") } |
参考
- Kotlin Coroutines 公式ページ ― https://kotlinlang.org/docs/coroutines-overview.html
- Android 開発者向け「構造化並行性」ガイド ― https://developer.android.com/kotlin/coroutines#structured-concurrency
1‑2. Structured Concurrency を意識したスコープの選択
viewModelScope–ViewModelが破棄されると自動的に子コルーチンがキャンセルされます。lifecycleScope/repeatOnLifecycle–Fragment・Activityのライフサイクルに合わせた安全な実行が可能です。
これらのスコープを使わずに GlobalScope.launch {} を乱用すると、画面遷移後もタスクが残りメモリリークやクラッシュの原因になりますので避けましょう。
launch と async の違いと実装例
| 特徴 | launch |
async |
|---|---|---|
| 戻り値 | なし(Job) |
Deferred<T>(結果取得可能) |
| 用途 | fire‑and‑forget:UI に直接影響しないバックグラウンド処理 | 結果が必要な非同期計算 |
| キャンセル伝搬 | 親スコープに自動で従う | 同上 |
実装例(ViewModel 内)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MainViewModel( private val repository: UserRepository ) : ViewModel() { // UI に直接影響しないキャッシュ更新(fire‑and‑forget) fun refreshCache() = viewModelScope.launch(Dispatchers.IO) { repository.updateAll() } // DB から件数を取得し、LiveData として公開 val userCount = liveData { // async + await による結果取得 val count = viewModelScope.async(Dispatchers.Default) { repository.getUserCount() }.await() emit(count) } } |
参考
- Android の構造化並行性ガイド ― https://developer.android.com/kotlin/coroutines#structured-concurrency
suspend 関数と Flow の基本的な使い方
2‑1. suspend 関数の定義と呼び出し
suspend は「中断可能」な関数で、ブロッキング API をすべて置き換えることが推奨されます。内部では withContext や delay などを組み合わせて非同期処理を書きます。
|
1 2 3 4 5 6 |
// ネットワーク呼び出しや DB アクセスの例 suspend fun fetchData(): String = withContext(Dispatchers.IO) { delay(500) // 模擬的な I/O 待ち時間 "Fetched result" } |
呼び出し側は launch / async の中で普通に使用できます。
|
1 2 3 4 5 |
viewModelScope.launch { val result = fetchData() _uiState.value = result } |
2‑2. Flow:Cold Stream とリアクティブ処理
Flow<T> は 遅延評価(Cold) の非同期データストリームです。購読 (collect) が開始された瞬間にだけ上流の処理が走り、キャンセルは自動的に伝搬します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// DAO 側で Flow を返す例 (Room が内部で実装) fun observeUsers(): Flow<List<User>> = dao.getAllUsersFlow() .filter { it.isNotEmpty() } // 不要データを除外 .map { users -> users.sortedBy { it.name } } // ソート // UI 側 (Fragment) で lifecycle‑aware に収集 viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { observeUsers().collect { list -> adapter.submitList(list) } } } |
参考
- Flow の公式ドキュメント ― https://developer.android.com/kotlin/flow
実践例① Retrofit + Coroutines でシンプルなネットワーク呼び出し
3‑1. Retrofit API を suspend 関数化
|
1 2 3 4 5 |
interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: Long): UserResponse // 自動的にコルーチンへ変換 } |
依存関係(
build.gradle.kts)
|
1 2 3 |
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") |
3‑2. エラーハンドリングは runCatching と Result で統一
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class UserRepository(private val api: ApiService) { suspend fun fetchUser(id: Long): Result<User> = runCatching { api.getUser(id).toDomain() }.recoverCatching { e -> when (e) { is HttpException -> throw ApiError(e.code(), e.message()) is IOException -> throw NetworkError(e) else -> throw e } } } |
3‑3. ViewModel での利用例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UserViewModel( private val repository: UserRepository ) : ViewModel() { private val _state = MutableStateFlow<UserUiState>(UserUiState.Loading) val state: StateFlow<UserUiState> = _state.asStateFlow() fun load(id: Long) = viewModelScope.launch { _state.value = UserUiState.Loading when (val result = repository.fetchUser(id)) { is Result.Success -> _state.value = UserUiState.Success(result.getOrThrow()) is Result.Failure -> _state.value = UserUiState.Error(result.exceptionOrNull()!!) } } } |
参考
- Retrofit の Kotlin Coroutines サポート ― https://square.github.io/retrofit/
実践例② Room DAO を suspend に置き換えて非同期 DB 操作
4‑1. DAO の定義(suspend 化)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(user: UserEntity) @Query("SELECT * FROM user WHERE id = :id") suspend fun getById(id: Long): UserEntity? @Delete suspend fun delete(user: UserEntity) // Flow 版の取得例(リアクティブ UI に便利) @Query("SELECT * FROM user") fun getAllUsersFlow(): Flow<List<UserEntity>> } |
4‑2. ViewModel での呼び出し
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UserEditViewModel( private val dao: UserDao ) : ViewModel() { // fire‑and‑forget:UI が結果を待たない保存処理 fun save(user: UserEntity) = viewModelScope.launch(Dispatchers.IO) { dao.insert(user) } // 結果が必要なときは async + await(ただし Flow でも代替可) val userLiveData = liveData { val user = viewModelScope.async(Dispatchers.IO) { dao.getById(42L) }.await() emit(user) } } |
参考
- Room の Coroutines サポート ― https://developer.android.com/training/data-storage/room/async-queries
実践例③ Lifecycle‑aware UI 更新と例外一元管理
5‑1. repeatOnLifecycle と lifecycleScope の組み合わせ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class UserListFragment : Fragment(R.layout.fragment_user_list) { private val viewModel: UserViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { uiState -> when (uiState) { is UserUiState.Success -> adapter.submitList(uiState.users) is UserUiState.Error -> showError(uiState.throwable) else -> Unit // Loading 等は省略 } } } } } } |
参考
-repeatOnLifecycleの公式解説 ― https://developer.android.com/reference/androidx/lifecycle/package-summary#repeatOnLifecycle
5‑2. CoroutineExceptionHandler による未捕捉例外の集約
|
1 2 3 4 5 6 7 8 9 10 |
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> Log.e("Coroutines", "Unhandled: ${exception.localizedMessage}", exception) // UI へエラーメッセージを流すなどの共通処理 } fun fetchAndShow() = viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { val data = repository.loadHeavyData() withContext(Dispatchers.Main) { ui.show(data) } } |
テストとパフォーマンス最適化
6‑1. runTest を用いた単体テスト(kotlinx‑coroutines‑test)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class UserRepositoryTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `fetchUser returns success`() = runTest { // Arrange val api = mock<ApiService>() whenever(api.getUser(any())).thenReturn(UserResponse(id = 1, name = "John")) val repo = UserRepository(api) // Act val result = repo.fetchUser(1L) // Assert assertTrue(result.isSuccess) assertEquals("John", result.getOrThrow().name) } } |
runTest は仮想タイムスケジューラを利用し、delay や withTimeout が即時に進むためテストが高速化します。
参考
-runTestの公式ドキュメント ― https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/run-test.html
6‑2. Dispatcher の適切な選択と過剰 launch 防止
| 種類 | 推奨シナリオ |
|---|---|
Dispatchers.Default |
CPU バウンド(計算・画像処理) |
Dispatchers.IO |
I/O バウンド(ネットワーク、DB、ファイル) |
過剰に launch するとスレッドプールが飽和し、パフォーマンス低下や OOM が起きやすくなります。Debounce・distinctUntilChanged と組み合わせて必要最小限のコルーチンだけを生成しましょう。
|
1 2 3 4 5 6 7 8 |
searchQuery .debounce(300) // 連続入力をまとめる .filter { it.isNotBlank() } .flatMapLatest { q -> repository.search(q).asFlow() } .flowOn(Dispatchers.Default) // 重い検索ロジックは Default に委譲 .onEach { results -> _state.value = results } .launchIn(viewModelScope) |
まとめ(重複しないシンプルな要点)
- Gradle:
kotlinx-coroutines-android:1.7.3とkotlinx-coroutines-testを追加し、compileSdk = 34でビルド。 - 構造化並行性は必ず
viewModelScope,lifecycleScope, またはrepeatOnLifecycle内でコルーチンを起動。 launchとasyncの使い分け:結果が不要ならlaunch、必要ならasync+await(ただし Flow が適切なケースも多い)。suspend関数は単発非同期処理、Flow はストリームデータとして活用し、Dispatcher を明示的に指定。- Retrofit / Room の
suspend化でコードがシンプル化し、エラーハンドリングはrunCatchingとResultで統一。 - UI 側は lifecycle‑aware に:
repeatOnLifecycle+lifecycleScopeで自動キャンセルを実現し、例外はCoroutineExceptionHandlerに集約。 - テストは
runTestを利用して高速かつ確実に検証し、Dispatcher の選択とデバウンスで過剰なコルーチン生成を防止すれば、メモリ・CPU 負荷を抑えた安定アプリが構築できます。
これらのベストプラクティスをプロジェクトに組み込むことで、現在の Android 開発環境で推奨されている Coroutines の機能とパフォーマンスを最大限に活用できるはずです。