Lỗi
Lỗi này thường xảy ra khi bạn đang thực hiện các logic xử lý dữ liệu từ một REST API hoặc cơ sở dữ liệu Room. Bạn cố gắng đẩy kết quả lên giao diện người dùng (UI) thông qua LiveData, nhưng ứng dụng bị đóng ngay lập tức. Khi kiểm tra Logcat, bạn sẽ thấy stack trace như sau:
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)
Nguyên nhân
LiveData được thiết kế để nhận biết vòng đời (lifecycle-aware) và gắn kết chặt chẽ với giao diện người dùng. Vì bộ công cụ UI của Android không an toàn về luồng (thread-safe), framework này áp đặt một quy tắc nghiêm ngặt: mọi cập nhật trực tiếp lên UI phải diễn ra trên Main Thread (Luồng chính).
Phương thức setValue() là một hoạt động đồng bộ. Khi bạn gọi nó, LiveData sẽ kiểm tra luồng hiện tại ngay lập tức bằng hàm assertMainThread(). Nếu bạn đang ở trong một khối Dispatchers.IO, một Thread { ... }, hoặc một observer chạy ngầm của RxJava, việc kiểm tra này sẽ thất bại. Ngoại lệ này được tạo ra để ngăn chặn tình trạng race condition có thể làm hỏng trạng thái UI của bạn.
Cách khắc phục
Cách 1: Sử dụng postValue() cho Background Thread
Nếu bạn đã ở trong một background thread (luồng nền), cách sửa đơn giản nhất là thay thế setValue() bằng postValue(). Thay vì cập nhật giá trị ngay lập tức, postValue() sẽ lập lịch một tác vụ trên hàng đợi thông báo của Main Thread để cập nhật giá trị sớm nhất có thể.
// ❌ SAI: Sẽ bị crash vì nằm trong IO dispatcher
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.getProfile(id = 101)
_userData.value = result
}
}
// ✅ ĐÚNG: An toàn khi gọi từ bất kỳ background worker nào
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.getProfile(id = 101)
_userData.postValue(result)
}
}
Cách 2: Chuyển đổi Context trong Coroutines
Phát triển Android hiện đại ưu tiên sử dụng Coroutines. Nếu bạn cần đảm bảo giá trị được gán ngay lập tức trước khi chuyển sang dòng mã tiếp theo, hãy sử dụng withContext(Dispatchers.Main). Điều này giúp chuyển việc thực thi trở lại UI thread một cách rõ ràng.
fun loadSettings() {
viewModelScope.launch(Dispatchers.IO) {
val settings = database.settingsDao().getSettings()
// Chuyển sang Main để sử dụng thuộc tính .value nhanh hơn
withContext(Dispatchers.Main) {
_settings.value = settings
// Bạn có thể thực hiện các logic UI khác một cách an toàn tại đây
}
}
}
Cách 3: Chuyển đổi Thread trong RxJava
Khi làm việc với RxJava, bạn phải đảm bảo observer đang chạy trên main thread. Hãy sử dụng toán tử observeOn trước khi tương tác với các đối tượng LiveData.
repository.getOrders()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) // Quan trọng để đảm bảo an toàn cho LiveData
.subscribe({ orders ->
_orders.value = orders
}, { error ->
log.e("Không thể tải đơn hàng", error)
})
setValue() vs postValue(): Lưu ý quan trọng
Mặc dù postValue() rất tiện lợi, nhưng nó có một hành vi đặc biệt mà bạn cần lưu ý. Nó hoạt động bất đồng bộ. Nếu bạn gọi postValue("A") và sau đó là postValue("B") liên tiếp trước khi Main Thread kịp thực thi, các observer sẽ chỉ nhận được giá trị "B". Giá trị cũ sẽ bị ghi đè trong hàng đợi. Nếu mọi thay đổi trạng thái đều quan trọng, hãy sử dụng withContext(Dispatchers.Main) kết hợp với setValue().
Kiểm tra kết quả
Bạn có thể xác nhận việc khắc phục bằng cách kiểm tra hai điều sau:
- **Logcat:** Lọc theo từ khóa "IllegalStateException". Lỗi cụ thể liên quan đến background thread sẽ không còn xuất hiện khi tác vụ ngầm hoàn tất.
- **Strict Mode:** Kích hoạt `ThreadPolicy` trong class `Application` của bạn. Điều này giúp phát hiện các thao tác ghi ổ đĩa hoặc mạng vô tình thực hiện trên main thread trong khi bạn đang cố gắng sửa logic LiveData.
Mẹo phòng ngừa
Một quy tắc hữu ích là xử lý việc cập nhật LiveData ở "cấp cao nhất" của ViewModel. Nếu bạn sử dụng viewModelScope.launch mà không chỉ định dispatcher, nó sẽ mặc định là Dispatchers.Main, giúp việc sử dụng .value = trở nên an toàn. Chỉ sử dụng postValue() khi bạn đang ở trong một khối lệnh dành riêng cho các tác vụ nặng, chẳng hạn như Dispatchers.IO hoặc Dispatchers.Default.

