Một đoạn code kỳ vọng callback cập nhật UI chạy sau dòng kế tiếp, nhưng thứ tự quan sát lại ngược lại. Nếu lời gọi là runOnUiThread và bạn đã đứng trên UI thread, callback có thể được thực hiện ngay. Tên hàm nói về thread đích, không tự có nghĩa luôn trì hoãn công việc sang hàng đợi.
Bài đối chiếu Android API reference ngày 10/11/2026. Các API trong ví dụ có từ API level 1. Code Kotlin dành cho Android Views đượ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 đo thứ tự log thực tế. Các chuỗi thứ tự dưới đây là kỳ vọng cần kiểm tra theo hợp đồng API.
Hai đường đi khi caller đã ở main thread
Activity.runOnUiThread quy định action chạy ngay nếu thread hiện tại là UI thread; nếu không, action được đưa vào event queue của UI thread. Vì thế, cùng một hàm có thể có cách thực thi khác tùy nơi gọi.
Handler.post đưa Runnable vào message queue gắn với Handler. Ví dụ tạo Handler bằng main Looper để nói rõ thread đích, thay vì dựa vào một constructor tự đoán Looper.
Dùng hai nút để quan sát riêng từng đường
Đặt Activity này trong package dự án thử nghiệm, khai báo Activity trong manifest theo ứng dụng của bạn. Mỗi lần thử chỉ bấm một nút rồi đọc kết quả:
import android.app.Activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
class UiQueueDemoActivity : Activity() {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val report = TextView(this)
val immediate = Button(this).apply {
text = "runOnUiThread"
setOnClickListener {
val trace = mutableListOf("before")
runOnUiThread { trace.add("inside") }
trace.add("after")
report.text = trace.joinToString(" -> ")
}
}
val queued = Button(this).apply {
text = "Handler.post"
setOnClickListener {
val trace = mutableListOf("before")
val accepted = handler.post {
trace.add("inside")
report.text = trace.joinToString(" -> ")
}
trace.add("after")
report.text = trace.joinToString(" -> ")
if (!accepted) report.text = "Không đưa được vào hàng đợi"
}
}
val content = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(immediate)
addView(queued)
addView(report)
}
setContentView(content)
}
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
}
Hai callback của nút được framework gọi trên UI thread trong tình huống màn hình thông thường. Với runOnUiThread, thứ tự cần quan sát là before, inside, after. Với Handler.post được chấp nhận vào queue, khi Runnable được xử lý, trace dự định là before, after, inside. Dòng report trước đó có thể đã được ghi với hai phần tử; không coi nó là một phép đo frame hoặc bằng chứng chắc chắn người dùng đã nhìn thấy trạng thái trung gian.
Post thành công chưa có nghĩa công việc đã hoàn tất
Giá trị boolean của post cho biết Runnable có được đưa vào queue hay không. Nó không chứa kết quả của Runnable và không chứng minh callback đã chạy xong. Tài liệu nêu trường hợp Looper đang thoát có thể khiến thao tác đưa vào queue thất bại.
Trong demo, Handler thuộc riêng Activity và callbacks được dọn ở onDestroy. Đây chỉ là ví dụ nhỏ để quan sát thứ tự; nó không triển khai một cơ chế lưu việc qua process death hoặc khôi phục màn hình. Khi công việc có vòng đời dài, cần thiết kế nơi sở hữu và hủy công việc phù hợp với ứng dụng.
Cả hai đều không tự chuyển tác vụ nặng sang background
Handler được gắn main Looper nên Runnable của nó vẫn chạy trên main thread. Đổi từ runOnUiThread sang post không làm phép xử lý ảnh hoặc vòng lặp nặng trở thành công việc background. Nó chỉ đổi cách đặt action vào luồng thực thi ở trường hợp đang bàn.
Không dùng Thread.sleep trên UI thread để cố tạo ra thứ tự mong muốn trong bài thử. Mục tiêu là hiểu hợp đồng lập lịch, không làm màn hình đứng để có một log dễ nhìn hơn. Nếu cần hoàn thành một việc rồi mới làm việc sau, chọn một cách biểu diễn phụ thuộc kết quả rõ ràng thay vì suy từ tên API.
Checklist khi điều tra thứ tự
Xác nhận thread ở nơi gọi, Looper của Handler và thời điểm Activity còn hợp lệ. Ghi trace trước, trong và sau callback thay vì chỉ một dòng “đã gọi hàm”. Tách việc đăng Runnable, thực thi Runnable và vẽ giao diện thành những mốc khác nhau trong cách diễn giải.
Thử hai nút riêng trên thiết bị mục tiêu và kiểm tra sau khi rời màn hình. Ví dụ giúp bạn đặt đúng câu hỏi về chạy ngay hay xếp hàng; nó không cam kết thời gian chờ cố định hoặc thay thế kiểm thử vòng đời của sản phẩm.

