Sửa lỗi android.view.WindowManager$BadTokenException Khi Hiển Thị Dialog trên Android

intermediate📱 Android2026-04-22| Android (API 21+), Java/Kotlin, tất cả phiên bản Android bao gồm Android 10/11/12/13/14

Error Message

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@... is not valid; is your activity running?
#android#dialog#activity#lifecycle#windowmanager

Lỗi Gặp Phải

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@4f3a2c1 is not valid; is your activity running?

Lỗi crash này thường xuất hiện trên Crashlytics ngay khi một dialog cố gắng hiển thị. Kiểm tra stack trace — hầu như luôn trỏ đến dialog.show() nằm trong một async callback hoặc Handler.postDelayed().

Nguyên Nhân

Mỗi dialog trong Android cần một window token từ context của nó — thường là Activity. Khi bạn gọi dialog.show(), Android cố gắng gắn cửa sổ dialog vào token đó. Activity đã bị hủy = token không hợp lệ = crash.

Các nguyên nhân phổ biến:

  • Một network callback hoặc AsyncTask hoàn tất sau khi người dùng đã nhấn Back. Dialog cố hiển thị trên Activity đã bị destroy.
  • Gọi dialog.show() sau khi finish() đã được thực thi.
  • Truyền getApplicationContext() vào constructor của dialog — application context không có window token.
  • Xoay màn hình giữa chừng khi đang request: Activity cũ đã biến mất, nhưng callback vẫn giữ tham chiếu cũ đến nó.
  • Fragment dialog hiển thị sau onSaveInstanceState() — anh em họ gần của lỗi IllegalStateException.

Cách Sửa Từng Bước

1. Kiểm Tra Activity Còn Sống Trước Khi Hiển Thị Dialog

Thêm đoạn kiểm tra này trước mỗi lần gọi dialog.show():

// Kotlin
fun showDialogSafely(activity: Activity, dialog: AlertDialog) {
    if (!activity.isFinishing && !activity.isDestroyed) {
        dialog.show()
    }
}
// Java
private void showDialogSafely(Activity activity, AlertDialog dialog) {
    if (!activity.isFinishing() && !activity.isDestroyed()) {
        dialog.show();
    }
}

Tại sao cần cả hai? isFinishing() chuyển thành true ngay khi finish() được gọi — nhưng Activity chưa thực sự biến mất. isDestroyed() bao phủ trạng thái đã bị hủy hoàn toàn. Bạn cần cả hai.

2. Dùng Activity Context, Không Dùng Application Context

Dialog cần một UI context có window token. getApplicationContext() không có — sẽ luôn crash:

// SAI — sẽ crash
AlertDialog.Builder builder = new AlertDialog.Builder(getApplicationContext());

// ĐÚNG
AlertDialog.Builder builder = new AlertDialog.Builder(MyActivity.this);
// Kotlin:
AlertDialog.Builder(this@MyActivity)

3. Hiển Thị Dialog Trên Main Thread

Background thread không thể đụng vào UI. Nếu dialog nằm trong callback hoặc async block, hãy chuyển về main thread trước:

// Kotlin — trong coroutine hoặc callback
runOnUiThread {
    if (!isFinishing && !isDestroyed) {
        AlertDialog.Builder(this)
            .setMessage("Hoàn tất")
            .show()
    }
}
// Java — dùng Handler
new Handler(Looper.getMainLooper()).post(() -> {
    if (!isFinishing() && !isDestroyed()) {
        new AlertDialog.Builder(MyActivity.this)
            .setMessage("Hoàn tất")
            .show();
    }
});

4. Sửa Pattern Async/Coroutine

Kiểm tra lifecycle thủ công chỉ là giải pháp tạm thời. Cách sửa thực sự là gắn tác vụ async vào lifecycle của Activity để nó tự hủy khi màn hình đóng:

// Kotlin — lifecycleScope tự hủy khi Activity bị destroy
lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) {
        fetchData() // chạy trên background thread
    }
    // Quay lại main thread — Activity được đảm bảo còn sống ở đây
    AlertDialog.Builder(this@MyActivity)
        .setMessage(result)
        .show()
}

Không cần kiểm tra isFinishing(). lifecycleScope tự hủy khi Activity chết — coroutine không bao giờ chạy đến show().

5. Với Fragment — Dùng DialogFragment

Hiển thị dialog từ Fragment? Đừng gọi dialog.show() trực tiếp. Dùng DialogFragment và kiểm tra Fragment còn được gắn không:

// Kiểm tra fragment còn được gắn vào Activity
if (isAdded && !requireActivity().isFinishing) {
    MyDialogFragment().show(parentFragmentManager, "my_dialog")
}

Nếu bắt buộc phải hiển thị sau khi state đã được lưu, dùng commitAllowingStateLoss() trên FragmentTransaction — nhưng chỉ khi việc mất trạng thái dialog là chấp nhận được.

6. Dismiss Dialog Trong onDestroy

Đang giữ tham chiếu đến dialog? Dọn dẹp trước khi Activity chết, nếu không Android sẽ làm thay bạn — bằng một crash:

private var progressDialog: AlertDialog? = null

override fun onDestroy() {
    progressDialog?.dismiss()
    progressDialog = null
    super.onDestroy()
}

Kiểm Tra Sau Khi Sửa

  • Kích hoạt tác vụ async, rồi ngay lập tức nhấn Back hoặc xoay màn hình. Không crash = xong.
  • Sau khi deploy, theo dõi Crashlytics — tỉ lệ lỗi BadTokenException phải về zero trong một hai ngày.
  • Chạy adb logcat | grep BadTokenException trong khi thử các edge case. Mục tiêu là không có gì xuất hiện.
  • Trong unit test, kiểm tra rằng mọi đường dẫn code hiển thị dialog đều kiểm tra trạng thái Activity trước khi gọi show().

Tóm Tắt Nhanh

  • Luôn kiểm tra: !activity.isFinishing() && !activity.isDestroyed() trước dialog.show()
  • Không bao giờ dùng: getApplicationContext() làm context cho dialog
  • Ưu tiên dùng: lifecycleScope.launch thay vì thread thô hoặc AsyncTask cho tác vụ async có đụng đến UI
  • Dismiss trong: onDestroy() nếu bạn đang giữ tham chiếu đến dialog
  • Luôn kiểm thử: xoay màn hình trên mọi màn hình kết hợp tác vụ async với dialog

Related Error Notes