viewModelScope は、androidx.lifecycle ライブラリの組み込み CoroutineScope で、ViewModel のライフサイクルに関連付けられ、クリア時に自動的にキャンセルされます。Google Android Developers(2025 年)によると、viewModelScope は MVVM アーキテクチャでコルーチンを起動する標準的なメカニズムであり、メモリリークのリスクなく安全な非同期操作を保証します。ViewModelScope はデフォルトで Dispatchers.Main を使用し、その内部のすべての IO 操作は withContext を介して実行する必要があります。
重要なポイント
viewModelScope は、ViewModel インターフェースの拡張プロパティであり、lifecycle-viewmodel-ktx ライブラリ(バージョン 2.1.0 以降)で追加されました。ViewModel のライフサイクルに関連付けられた、すぐに使用できる CoroutineScope を提供します。
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
スコープは最初のアクセス時に遅延(lazy)作成され、setTag を介してキャッシュされます。SupervisorJob を使用するため、1 つの子コルーチンでの例外が他のコルーチンをキャンセルすることはありません。デフォルトのディスパッチャーは Dispatchers.Main.immediate で、既に Main スレッド上にある場合は追加のディスパッチなしでメインスレッドでコードを実行します。
ViewModel がライフサイクルを離れると(Activity の終了または Fragment の削除)、システムは clear() を呼び出し、onCleared() をトリガーします。このコールバックで、viewModelScope は Job をキャンセルし、すべてのアクティブなコルーチンを再帰的に終了します。このメカニズムは Closeable インターフェースを介して実装され、スコープの Job は自動クローズのリソースとして登録されます。
viewModelScope の ViewModel ライフサイクルへの関連付けメカニズムは、タグ付けと onCleared コールバックに基づいています。段階的に見ていきましょう。
ViewModel が viewModelScope.launch { ... } を実行すると、getter は JOB_KEY タグの下にスコープが既に保存されているかどうかを確認します。スコープが存在しない場合、新しい CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) インスタンスが作成されます。スコープは内部タグマップを介して ViewModel 内に保存されます。
viewModelScope.launch または viewModelScope.async を介して起動されたすべてのコルーチンは、スコープの SupervisorJob の子になります。これらはメインスレッドで実行されます(withContext を介して別のディスパッチャーが指定されていない限り)。ViewModel が存続している限り、コルーチンはアクティブ、一時停止、または完了状態になります。
システムが ViewModel を破棄すると、ViewModel.clear() が呼び出されます。clear() 内部では、次の処理が行われます:
画面が回転すると Activity は再作成されますが、ViewModel は存続します(ViewModelStoreOwner のおかげです)。つまり、viewModelScope はアクティブなままで、コルーチンは中断されることなく実行を続けます。Activity の再作成後も、同じ ViewModel(および同じスコープ)が再利用されるため、データの読み込みが最初からやり直されることはありません。
MVVM(Model-View-ViewModel)は、Google が推奨する Android アプリケーションのアーキテクチャです。viewModelScope は、非同期操作の実行者として中心的な役割を果たします。
| 層 | コンポーネント | viewModelScope の役割 |
|---|---|---|
| UI | Activity / Fragment | ViewModel からの StateFlow/LiveData を監視 |
| ViewModel | ViewModel | viewModelScope を介してコルーチンを起動、UI 状態を管理 |
| Repository | Repository | viewModelScope のコルーチンから呼び出される suspend 関数を提供 |
| Data | DAO / Api | 実際のリクエストを実行(Room、Retrofit) |
ViewModel は viewModelScope を介してコルーチンを起動し、その内部で Repository の suspend 関数 を呼び出します。結果は StateFlow に変換され、UI 層によって監視されます。この設計により、明確な責任の分離と各層の独立したテスト容易性が保証されます。
コルーチンが Fragment から起動された場合、画面回転時に Fragment の破棄とともにキャンセルされます。ViewModel は回転後も存続するため、そのスコープで起動されたコルーチンは実行を継続します。これが、データ読み込み時における lifecycleScope に対する viewModelScope の主な利点です。
Kotlin を使用した Android アプリケーションにおける viewModelScope の 3 つの実践的なシナリオを見てみましょう。
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
init ブロックで、プロフィールの読み込みが直ちに開始されます。コルーチンはデフォルトでメインスレッドで実行されます。リポジトリは suspend 関数内でネットワークリクエストに withContext(Dispatchers.IO) を使用するため、ViewModel はスレッド切り替えを気にする必要がありません。
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
UI の状態は sealed class UiState を使用して記述されます。ViewModel は変更のたびに状態を更新します。Fragment は StateFlow に購読し、現在の状態にのみ反応し、以前の回転からの古い呼び出しを無視します。
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
新しい検索クエリごとに、以前のコルーチンがキャンセルされます。delay(300) はデバウンスを実装し、300 ミリ秒の非アクティブ後にのみ検索が実行されます。これによりサーバーの負荷が軽減され、古い結果が防止されます。
どちらのスコープも AndroidX Lifecycle ライブラリによって提供されますが、異なるライフサイクルに関連付けられています。選択はタスクの種類によって異なります。
| 特性 | viewModelScope | lifecycleScope |
|---|---|---|
| 所有者 | ViewModel | LifecycleOwner(Activity/Fragment) |
| 回転時にキャンセル | いいえ(ViewModel は存続) | はい(Activity は再作成) |
| デフォルトディスパッチャー | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| 利用可能場所 | ViewModel | Activity、Fragment、Service |
| 典型的な使用例 | データ読み込み、ビジネスロジック | UI 操作、アニメーション |
Google は、すべてのデータ読み込みおよび処理タスクに viewModelScope を使用することを推奨しています。lifecycleScope は、特定の UI ライフサイクルの瞬間に関連付けられた操作(初回画面表示時のアニメーション開始や、画面離脱時に停止すべき位置情報更新の購読など)に使用する必要があります。
十分に文書化された Android API であっても、開発者は特有の間違いを犯します。最も一般的な 4 つの問題を見てみましょう。
最も厄介な間違いは、ViewModel がクリアされた後に StateFlow や LiveData を更新しようとすることです。viewModelScope は onCleared() でキャンセルされますが、コルーチンは実際のキャンセルが有効になる前にコードを実行する可能性があります。確認には isActive を使用するか、catch ブロックの完了に依存してください。
viewModelScope は内部的に SupervisorJob を使用して、コルーチン間のエラーを分離します。ただし、viewModelScope.launch 内で独自の Job() を持つコルーチンを起動した場合、そのコルーチンは SupervisorJob の子になりますが、他のコルーチンのエラーによるキャンセルからは保護されません。
viewModelScope に厳密な制限はありませんが、数千のアクティブなコルーチンはシステムを遅くする可能性があります。長いデータリストの場合は、各アイテムに個別のコルーチンを作成する代わりに、Flow を collectLatest とともに使用してください。
viewModelScope の代わりに誤って GlobalScope をインポートした場合、ViewModel のクリア時にコルーチンはキャンセルされません。これによりメモリリークや潜在的なクラッシュが発生します。特に Fragment のサブクラスでは、常にコルーチンが viewModelScope を介して起動されていることを確認してください。
よくある質問
viewModelScope のディスパッチャーを直接変更することはできません。Dispatchers.Main.immediate としてハードコードされています。ただし、コルーチン内で withContext を介して別のディスパッチャーに切り替えることは可能です。テストでディスパッチャーを変更するには、Rule を介して TestDispatcher を使用してください。
スコープを Repository に渡さないでください。アーキテクチャの原則に反します。Repository は suspend 関数 を公開し、ViewModel 自体が viewModelScope を介してコルーチンを管理する必要があります。Repository がスコープを必要とする場合は、Clean Architecture を採用してアーキテクチャを再検討してください。
SupervisorJob は、1 つのコルーチンでの例外(複数の独立したリクエストのうち 1 つでの読み込みエラーなど)が他のコルーチンをキャンセルしないことを保証します。これは、異なる画面が独立したデータを読み込む ViewModel のシナリオに適合します。
はい、viewModelScope は UI の種類(View System または Jetpack Compose)に関係なく、任意の ViewModel で利用できます。Compose でもコルーチンは viewModelScope を介して起動され、UI エフェクトには LaunchedEffect と rememberCoroutineScope が使用されます。
viewModelScope.cancel() を呼び出すと、スコープは直ちにキャンセルされ、すべてのアクティブなコルーチンが CancellationException で終了します。その後 viewModelScope.launch を呼び出すと、getter への次回アクセス時に新しいスコープが自動的に作成されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。