Màn hình vừa nạp dữ liệu cài đặt và gán isChecked, nhưng listener lại chạy logic lưu như thể người dùng vừa bấm checkbox. Tình huống này không nhất thiết là callback bị gọi hai lần. Bạn có thể đang dùng một sự kiện đổi trạng thái để suy ra nguồn gốc của thao tác.
Bài đối chiếu Android API reference và mã nguồn AOSP ngày 09/11/2026. Ví dụ dùng Android Views bằng Kotlin, với API setChecked và listener có từ API level 1. Code được rà theo tài liệu, chưa biên dịch bằng Android SDK, chạy emulator, thiết bị hoặc kiểm tra accessibility.
Callback báo điều gì đã thay đổi?
CompoundButton cung cấp setChecked và setOnCheckedChangeListener. CheckBox kế thừa hành vi này. Callback đăng ký để nhận khi trạng thái checked đổi, không có hợp đồng rằng mọi callback đều bắt nguồn từ thao tác chạm.
Trong mã nguồn AOSP CompoundButton, setChecked kiểm tra giá trị mới khác giá trị cũ rồi gọi listener đã đăng ký. Vì thế, phép gán isChecked trong Kotlin cần được đọc như một đường cập nhật trạng thái có thể phát thông báo.
Dùng màn hình nhỏ để quan sát nguồn thay đổi
Thêm Activity này vào package ứng dụng thử nghiệm và khai báo Activity trong manifest theo dự án. Không cần layout XML hoặc thư viện AndroidX cho ví dụ:
import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.TextView
class CheckedDemoActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var callbacks = 0
val report = TextView(this).apply {
text = "Callback: 0"
}
val checkBox = CheckBox(this).apply {
text = "Nhận thông báo"
isChecked = false
}
checkBox.setOnCheckedChangeListener { _, checked ->
callbacks += 1
report.text = "Callback: $callbacks, checked: $checked"
}
val setTrue = Button(this).apply {
text = "Gán true bằng code"
setOnClickListener { checkBox.isChecked = true }
}
val setFalse = Button(this).apply {
text = "Gán false bằng code"
setOnClickListener { checkBox.isChecked = false }
}
val content = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(checkBox)
addView(setTrue)
addView(setFalse)
addView(report)
}
setContentView(content)
}
}
Giá trị false ban đầu được đặt trước khi gắn listener. Hai nút dùng code để đổi trạng thái checkbox, còn thao tác bấm trực tiếp vào checkbox là một đường tương tác khác. Dòng report giúp quan sát callback mà không gửi network hoặc ghi dữ liệu thật.
Kế hoạch kiểm tra theo từng bước
Ở lần mở đầu, ghi nhận checkbox chưa chọn và số callback hiển thị bằng 0. Bấm nút gán true: trạng thái cần chuyển sang chọn và listener dự định tăng bộ đếm. Bấm nút true thêm một lần khi giá trị đã true: theo đường setChecked đang đối chiếu, không có thay đổi checked mới để thông báo.
Sau đó bấm false rồi bấm trực tiếp checkbox. So sánh giá trị checked trong report với giao diện. Đây là các kết quả cần xác minh khi chạy ứng dụng của bạn, không phải log thiết bị đã thu trong bài. Nếu thêm subclass hoặc binding khác, cần kiểm tra thêm hành vi thực tế của lớp đó.
Tách việc render state khỏi ý định lưu dữ liệu
Một listener có thể dùng để cập nhật phần UI phụ thuộc trạng thái, nhưng việc đặt lời gọi lưu trực tiếp ở đó cần chính sách rõ. Khi ứng dụng nạp state từ repository rồi render vào checkbox, không nên mặc định đó là một yêu cầu mới cần ghi ngược lại repository.
Một cách tổ chức là có luồng render dữ liệu và luồng xử lý ý định người dùng riêng, rồi kiểm tra cách binding của màn hình kết nối hai luồng ấy. Nếu dùng cờ chặn trong lúc render hoặc tháo/gắn listener có kiểm soát, cần bảo đảm trạng thái được khôi phục khi có lỗi và không làm mất những thay đổi cần xử lý. Bài không cung cấp một cơ chế chặn tổng quát cho mọi kiến trúc.
Đừng thay thế việc phân biệt nguồn cập nhật bằng giả định “chỉ thao tác chạm mới là người dùng”. Màn hình còn có thể được điều khiển qua bàn phím hoặc công cụ hỗ trợ tiếp cận. Hãy kiểm tra cách tương tác mục tiêu thay vì suy ra ý định từ một dấu hiệu đầu vào đơn lẻ.
Lỗi thường gặp và phạm vi ví dụ
- Nạp dữ liệu rồi thấy lệnh lưu chạy: rà đường gán isChecked và thời điểm listener được đăng ký.
- Đổi lại checked ngay trong listener để đồng bộ hai phía: xem có vòng cập nhật hoặc trạng thái khó theo dõi không.
- Chỉ đếm callback mà không nhìn giá trị: thêm cả checked và nguồn cập nhật trong dữ liệu thử nghiệm.
- Coi ví dụ giữ state qua process death: Activity này không triển khai lưu/khôi phục dữ liệu.
Hãy bắt đầu bằng việc gọi đúng tên sự kiện: đây là thông báo đổi checked. Khi phần render và ý định lưu có hợp đồng riêng, việc khởi tạo hoặc nạp lại màn hình sẽ dễ kiểm soát hơn.


