Sửa lỗi IllegalStateException: Can not perform this action after onSaveInstanceState trong Android Fragment

intermediate📱 Android2026-05-03| Android (API 11+), Java/Kotlin, AndroidX Fragment 1.x, ảnh hưởng đến Activity và Fragment sử dụng FragmentManager

Error Message

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
#fragment#lifecycle#fragment-transaction#android-ui

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ước onResume()
  • Một background thread đăng lên main thread sau khi Activity đã bị tạm dừng
  • Một observer của LiveData hoặ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 viewLifecycleOwner trong 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 dialogcommitAllowingStateLoss() 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 IllegalStateException nà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
  • lifecycleScopeviewModelScope — Kotlin coroutines trên Android
  • Activity Result API — thay thế cho startActivityForResult

Related Error Notes