TL;DR
Bạn đã gọi FragmentManager.commit() sau khi Android đã lưu trạng thái của Activity — thường xảy ra vì một async callback kích hoạt sau khi người dùng nhấn Home hoặc xoay màn hình. Có hai cách xử lý: thay commit() bằng commitAllowingStateLoss() khi việc mất transaction khi khôi phục là chấp nhận được, hoặc kiểm tra trạng thái lifecycle trước khi thực hiện commit.
Lỗi Đầy Đủ
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:...)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:...)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:...)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:...)
Nguyên Nhân
Khi Android gọi onSaveInstanceState(), nó chụp lại trạng thái Activity của bạn — bao gồm toàn bộ Fragment back stack. Sau khi bản chụp đó được tạo, FragmentManager từ chối mọi thay đổi tiếp theo đối với back stack. Nếu bạn vẫn cố thao tác, ứng dụng sẽ crash với lỗi này.
Những trường hợp thường gặp:
- Một callback của Retrofit hoặc coroutine trả về sau khi người dùng nhấn Home hoặc màn hình đã xoay
- Một dialog được commit bên trong
DialogInterface.OnClickListener - Fragment commit bên trong
onActivityResult(), vốn chạy trướconResume() - Một background thread đăng lên main thread sau khi Activity đã bị tạm dừng
- Một observer của
LiveDatahoặc luồng RxJava phát dữ liệu sau khi lifecycle đã không còn active
Cách Sửa 1: commitAllowingStateLoss() — Nhanh Gọn
Thay commit() bằng commitAllowingStateLoss(). Cách này bỏ qua hoàn toàn việc kiểm tra state-loss. Đánh đổi là: nếu Activity được tạo lại, transaction này sẽ không được thực hiện lại. Điều đó chấp nhận được với UI tạm thời như loading spinner — nhưng không phù hợp cho điều hướng thực sự.
// Trước
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
// Sau
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commitAllowingStateLoss()
Phiên bản Java:
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new MyFragment())
.commitAllowingStateLoss();
Chỉ dùng cách này cho các transaction thực sự tạm thời — ẩn progress bar, đóng loading dialog. Điều hướng và bất kỳ thứ gì người dùng cần thấy sau khi cấu hình thay đổi cần được sửa đúng cách.
Cách Sửa 2: Kiểm Tra Trạng Thái Lifecycle Trước Khi Commit
Bảo vệ commit để nó chỉ chạy khi Activity đang ở trạng thái an toàn.
// Kotlin — trong một Activity
if (!isFinishing && !isDestroyed) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
// Kotlin — trong một Fragment
if (isAdded && !requireActivity().isFinishing) {
parentFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
Với code dùng coroutine, lifecycle.withStarted gọn gàng hơn. Nó tạm dừng cho đến khi lifecycle đạt ít nhất trạng thái STARTED — tức là onStart() đã chạy — rồi mới thực thi khối lệnh.
lifecycleScope.launch {
lifecycle.withStarted {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
}
Yêu cầu lifecycle-runtime-ktx 2.4.0+. Nếu lifecycle không bao giờ đạt lại trạng thái STARTED (ví dụ Activity kết thúc), khối lệnh đơn giản sẽ không bao giờ chạy.
Cách Sửa 3: Dùng LiveData Với Lifecycle Owner Phù Hợp
Bị crash bên trong một observer của ViewModel? Hãy kiểm tra lifecycle owner bạn đang truyền vào. Trong một Fragment, this tham chiếu đến chính Fragment đó — nó tồn tại lâu hơn view. Hãy dùng viewLifecycleOwner thay thế để observer bị hủy cùng với view.
// Sai — observer vẫn còn sống ngay cả sau khi view bị hủy
viewModel.result.observe(this) { navigateToDetail(it) }
// Đúng — gắn với lifecycle của view trong Fragment
viewModel.result.observe(viewLifecycleOwner) { navigateToDetail(it) }
Với viewLifecycleOwner, observer sẽ tự động bị xóa khi Fragment không còn hiển thị. Không còn callback cũ kích hoạt vào một Fragment đã chết nữa.
Cách Sửa 4: Vấn Đề Thời Điểm Của onActivityResult()
onActivityResult() kích hoạt trước onResume(), nên onSaveInstanceState() đã chạy tại thời điểm đó. Commit Fragment ở đây sẽ crash. Hãy lưu lại hành động cần thực hiện và chạy nó khi bạn quay lại onResume().
private var pendingAction: (() -> Unit)? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == MY_REQUEST && resultCode == RESULT_OK) {
pendingAction = { showResultFragment() }
}
}
override fun onResume() {
super.onResume()
pendingAction?.invoke()
pendingAction = null
}
Trên API 29+ (hoặc khi dùng Activity Result API qua registerForActivityResult), cách giải quyết này không còn cần thiết — callback đã kích hoạt sau onResume() rồi.
Cách Sửa 5: Retrofit và Async Callback
Các network call là nguồn gốc số một gây ra lỗi này trên môi trường production. Một request mất 2–3 giây; người dùng vuốt thoát sau 1 giây. Callback trả về, cố commit một Fragment, và crash ngay.
// Tệ — commit mà không có nhận thức về lifecycle
retrofitService.getData().enqueue(object : Callback<Data> {
override fun onResponse(call: Call<Data>, response: Response<Data>) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ResultFragment.newInstance(response.body()!!))
.commit()
}
...
})
// Tốt hơn — hủy call khi màn hình chuyển sang background
override fun onStop() {
super.onStop()
pendingCall?.cancel()
}
Cách khắc phục lâu dài thực sự là chuyển sang coroutine với viewModelScope. Scope tự động hủy khi ViewModel bị xóa, nên fragment transaction thậm chí không bao giờ được lên lịch.
viewModelScope.launch {
val result = repository.getData() // tự động hủy nếu ViewModel bị xóa
_uiState.value = result
}
Kết hợp với StateFlow hoặc LiveData được observe qua viewLifecycleOwner và bạn đã loại bỏ hoàn toàn nhóm vấn đề này.
Nên Dùng Cách Nào?
- Async callback (Retrofit, RxJava) → Chuyển sang coroutine với
viewModelScope+LiveData/StateFlow - LiveData observer kích hoạt quá muộn → Chuyển sang
viewLifecycleOwnertrong Fragment của bạn - Vấn đề thời điểm onActivityResult → Dùng pattern pending action, hoặc chuyển sang Activity Result API
- Loading/progress dialog →
commitAllowingStateLoss()hoàn toàn phù hợp ở đây - Cần vá nhanh → Kiểm tra lifecycle (
isAdded && !isFinishing) +commitAllowingStateLoss()
Xác Minh Bản Sửa
- Tái hiện crash: kích hoạt hành động async, rồi ngay lập tức nhấn Home hoặc xoay màn hình trước khi nó hoàn thành.
- Áp dụng bản sửa và lặp lại các bước tương tự.
- Kiểm tra Logcat — không được có dòng
IllegalStateExceptionnào xuất hiện. - Xác nhận Fragment transaction đã được commit đúng khi resume hoặc đã bị bỏ qua gọn gàng, tùy theo cách tiếp cận của bạn.
- Chạy stress test với thao tác điều hướng nhanh và tải mạng nền:
adb logcat | grep IllegalStateExceptionđể phát hiện các trường hợp ngoại lệ bạn có thể bỏ sót.
Tài Liệu Tham Khảo
- AndroidX Fragment — tài liệu
FragmentManager - Hướng dẫn các thành phần nhận biết Lifecycle trên Android
lifecycleScopevàviewModelScope— Kotlin coroutines trên Android- Activity Result API — thay thế cho
startActivityForResult

