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
AsyncTaskhoà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 khifinish()đã đượ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ỗiIllegalStateException.
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
BadTokenExceptionphải về zero trong một hai ngày. - Chạy
adb logcat | grep BadTokenExceptiontrong 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ướcdialog.show() - Không bao giờ dùng:
getApplicationContext()làm context cho dialog - Ưu tiên dùng:
lifecycleScope.launchthay 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

