エラーの概要
このエラーは通常、REST APIやRoomデータベースからデータを取得するロジックの最中に発生します。取得した結果をLiveData経由でUIに反映させようとした際、アプリが突然終了します。Logcatを確認すると、以下のようなスタックトレースが表示されているはずです。
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:487)
at androidx.lifecycle.LiveData.setValue(LiveData.java:306)
at com.example.app.MyViewModel$fetchData$1.invokeSuspend(MyViewModel.kt:25)
原因
LiveDataはライフサイクルを認識するように設計されており、UIと密接に関連しています。AndroidのUIツールキットはスレッドセーフではないため、フレームワークは「UIへの直接的な更新はすべてメインスレッド(Main Thread)で行われなければならない」という厳格なルールを課しています。
setValue()メソッドは同期的に動作します。このメソッドを呼び出すと、LiveDataは内部でassertMainThread()を使用して現在のスレッドを即座にチェックします。もしDispatchers.IOブロック内、Thread { ... }内、あるいはRxJavaのバックグラウンドオブザーバー内にいる場合、このチェックに失敗します。この例外は、UIの状態を破損させる可能性のある競合状態(Race Condition)を防ぐために存在します。
解決方法
方法1:バックグラウンドスレッドでpostValue()を使用する
すでにバックグラウンドスレッド上にいる場合、最も簡単な修正方法はsetValue()をpostValue()に置き換えることです。postValue()は値を即座に更新するのではなく、メインスレッドのメッセージキューに「可能な限り早く値を更新する」というタスクをスケジュールします。
// ❌ 誤り:IOディスパッチャ内であるためクラッシュします
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.getProfile(id = 101)
_userData.value = result
}
}
// ✅ 正解:バックグラウンドワーカーから安全に呼び出せます
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.getProfile(id = 101)
_userData.postValue(result)
}
}
方法2:Coroutinesでコンテキストを切り替える
現代のAndroid開発ではCoroutinesが推奨されます。次のコード行に進む前に値を確実に設定したい場合は、withContext(Dispatchers.Main)を使用します。これにより、明示的に実行をUIスレッドに戻すことができます。
fun loadSettings() {
viewModelScope.launch(Dispatchers.IO) {
val settings = database.settingsDao().getSettings()
// メインスレッドに切り替えて、より高速な .value プロパティを使用する
withContext(Dispatchers.Main) {
_settings.value = settings
// ここで他のUIロジックも安全に実行できます
}
}
}
方法3:RxJavaのスレッド切り替え
RxJavaを使用している場合は、オブザーバーがメインスレッドで動作していることを確認する必要があります。LiveDataオブジェクトを操作する前に、observeOnオペレータを使用してください。
repository.getOrders()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // LiveDataの安全性のために不可欠
.subscribe({ orders ->
_orders.value = orders
}, { error ->
log.e("注文の読み込みに失敗しました", error)
})
setValue() vs postValue():注意点
postValue()は便利ですが、知っておくべき特有の動作があります。それは「非同期」であるということです。メインスレッドが実行される前にpostValue("A")、続けてpostValue("B")を高速に呼び出した場合、オブザーバーは「B」のみを受け取ります。前の値はキュー内で上書きされてしまいます。すべての状態変化を確実に反映させる必要がある場合は、withContext(Dispatchers.Main)とsetValue()を組み合わせて使用してください。
修正の確認
以下の2点を確認することで、修正が正しく行われたか判断できます。
- **Logcat:** キーワード「IllegalStateException」でフィルタリングします。バックグラウンドタスクが完了した際、バックグラウンドスレッドに関する特定のエラーが表示されなくなっているはずです。
- **Strict Mode:** `Application`クラスで`ThreadPolicy`を有効にします。これにより、LiveDataのロジックを修正している最中に、メインスレッドで誤ってディスク書き込みやネットワーク通信を行ってしまうミスを検出しやすくなります。
予防策
経験則として、LiveDataの更新はViewModelの「トップレベル」で処理するのが良い方法です。ディスパッチャを指定せずにviewModelScope.launchを使用する場合、デフォルトでDispatchers.Mainが使用されるため、.value =を安全に使用できます。postValue()は、Dispatchers.IOやDispatchers.Defaultなどの重い処理専用のブロック内に明示的にいる場合にのみ使用するようにしましょう。

