Khắc phục lỗi Crash 'Fragment not attached to a context' trong Android

intermediate📱 Android2026-06-27

Error Message

java.lang

Cuộc gọi đánh thức lúc 2 giờ sáng: Sự cố Crash trên Production

Bảng điều khiển Firebase Crashlytics của bạn đang bùng nổ các báo cáo lỗi. Đang là giữa đêm, và một lỗi duy nhất chiếm tới 40% tổng số vụ crash hàng ngày của bạn. Người dùng báo cáo rằng ứng dụng bị đóng ngay khi họ xoay màn hình hoặc nhấn nút quay lại trong khi trang đang tải. Khi đi sâu vào stack trace, bạn tìm thấy thủ phạm: một lời gọi phụ thuộc vào context được kích hoạt chỉ chậm vài mili giây.

java.lang.IllegalStateException: Fragment not attached to a context.
    at androidx.fragment.app.Fragment.requireContext(Fragment.java:911)
    at androidx.fragment.app.Fragment.getResources(Fragment.java:975)
    at androidx.fragment.app.Fragment.getString(Fragment.java:997)

Vụ crash này là một cơn đau đầu điển hình về lifecycle. Nó xảy ra khi một Fragment cố gắng lấy một string resource hoặc truy cập một system service sau khi nó đã bị tách (detached) khỏi Activity máy chủ. Thông thường, một tác vụ bất đồng bộ—như một lệnh gọi Retrofit với độ trễ 500ms—hoàn tất công việc và cố gắng cập nhật UI, mà không biết rằng người dùng đã điều hướng sang trang khác.

Quá trình Debug: Săn lùng bóng ma

Để ngăn chặn tình trạng này, bạn cần hiểu về "tử lộ" của Fragment. Khi người dùng thoát khỏi một màn hình, FragmentManager sẽ gọi onDetach(). Tại thời điểm này, getContext() trả về null. Tuy nhiên, các phương thức như requireContext() hoặc getString() thì ít "khoan dung" hơn; chúng yêu cầu một context không null và sẽ ném ra một IllegalStateException nếu không tìm thấy.

Hãy kiểm tra codebase của bạn để tìm cái bẫy phổ biến này:

// Pattern nguy hiểm
viewModel.userData.observe(viewLifecycleOwner) { data ->
    apiService.fetchDetails(data.id).enqueue(object : Callback<Detail> {
        override fun onResponse(call: Call<Detail>, response: Response<Detail>) {
            // Nếu người dùng nhấn 'Back' trước khi server phản hồi,
            // dòng tiếp theo sẽ làm crash tiến trình.
            val message = getString(R.string.success_message)
            showToast(message)
        }
        override fun onFailure(call: Call<Detail>, t: Throwable) {}
    })
}

Các giải pháp đã được kiểm chứng

Bạn có nhiều cách để xử lý vấn đề này, từ các bước kiểm tra phòng vệ nhanh chóng đến các kiến trúc hiện đại.

1. Chốt chặn phòng vệ

Cách khắc phục nhanh nhất là xác minh Fragment vẫn còn "sống" trước khi chạm vào tài nguyên. Sử dụng flag isAdded hoặc kiểm tra null trên context. Nó đơn giản nhưng hiệu quả.

override fun onResponse(call: Call<Detail>, response: Response<Detail>) {
    if (!isAdded || context == null) return 

    val message = getString(R.string.success_message)
    showToast(message)
}

2. Truy cập an toàn với Null Safety của Kotlin

Tránh sử dụng requireContext() bên trong các callback. Thay vào đó, hãy sử dụng getContext() có thể null kết hợp với let. Điều này đảm bảo mã của bạn chỉ thực thi nếu Fragment hiện đang được gắn vào một UI host hợp lệ.

context?.let { safeContext ->
    val message = safeContext.getString(R.string.success_message)
    Toast.makeText(safeContext, message, Toast.LENGTH_SHORT).show()
}

3. Coroutines nhận biết Lifecycle (Khuyên dùng)

Nếu bạn vẫn đang sử dụng GlobalScope hoặc MainScope tiêu chuẩn trong Fragment của mình, hãy dừng lại. Phát triển Android hiện đại dựa vào viewLifecycleOwner.lifecycleScope. Scope này tự động hủy bỏ bất kỳ công việc nào đang diễn ra ngay khi view của Fragment bị hủy. Không còn job nào hoạt động nghĩa là không còn lỗi crash bị trì hoãn.

viewLifecycleOwner.lifecycleScope.launch {
    val result = repository.getData()
    // Mã này được đảm bảo sẽ không chạy nếu view đã biến mất
    binding.textView.text = getString(R.string.data_loaded)
}

Để kiểm soát chặt chẽ hơn nữa, hãy sử dụng repeatOnLifecycle. Điều này hoàn hảo để thu thập (collect) Flow chỉ khi Fragment ở một trạng thái cụ thể, chẳng hạn như STARTED.

4. Tạo một Extension có thể tái sử dụng

Bạn mệt mỏi với việc viết if (isAdded)? Hãy chuẩn hóa các bước kiểm tra an toàn của bạn bằng một extension function gọn gàng. Điều này giúp logic nghiệp vụ của bạn dễ đọc hơn trong khi che đi các mã lặp lại (boilerplate).

fun Fragment.withContext(block: (Context) -> Unit) {
    val ctx = context
    if (isAdded && ctx != null) {
        block(ctx)
    }
}

Xác minh: Cách Stress Test bản sửa lỗi

Các lỗi crash liên quan đến lifecycle nổi tiếng là khó bắt vì chúng phụ thuộc vào thời điểm chính xác. Để chứng minh bản sửa lỗi của bạn hoạt động, bạn cần ép buộc các điều kiện gây lỗi xảy ra.

- **Bật "Don't keep activities":** Tìm tùy chọn này trong Developer Options. Nó buộc hệ thống phải hủy Activity của bạn ngay khi bạn rời khỏi, làm lộ ra bất kỳ tham chiếu nào còn sót lại.
- **Giới hạn băng thông mạng:** Sử dụng trình giả lập để mô phỏng kết nối 3G chậm. Kích hoạt một cuộc gọi mạng, sau đó nhấn nút quay lại liên tục.
- **Theo dõi Logcat:** Quan sát lỗi `IllegalStateException`. Nếu ứng dụng vẫn hoạt động khi điều hướng nhanh, các câu lệnh bảo vệ (guard clauses) của bạn đang làm tốt nhiệm vụ.

Lời kết

Fragment chỉ là tạm thời. Đừng bao giờ giả định rằng chúng sẽ tồn tại mãi chỉ vì một tác vụ nền vẫn đang chạy. Bằng cách liên kết công việc của bạn với viewLifecycleOwner và ưu tiên sử dụng getContext() có thể null thay vì requireContext() quá khắt khe, bạn có thể loại bỏ một trong những nguyên nhân phổ biến nhất gây mất ổn định trên Android.

Related Error Notes