Back Press Handling là cơ chế chặn và xử lý nút Quay lại hệ thống trong Android, xác định hành động nào được thực hiện khi nhấn nút. Theo Android Developers (2024), bắt đầu từ Android 11, phương thức onBackPressed() truyền thống đã được thay thế bằng OnBackPressedDispatcher. API mới cho phép các thành phần ở mọi cấp độ trong hệ thống phân cấp chặn nhấn nút, không chỉ Activity. Sự khác biệt chính — hỗ trợ nhiều callback trong chuỗi có ưu tiên.
Những điểm chính
Back Press Handling là một cơ chế hệ thống trong Android xác định điều gì xảy ra khi người dùng nhấn nút Quay lại phần cứng hoặc phần mềm. Tùy thuộc vào ngữ cảnh, nhấn nút có thể: đóng màn hình hiện tại và quay lại màn hình trước, ẩn bàn phím, đóng Drawer hoặc Bottom Sheet, thoát khỏi ứng dụng nếu người dùng đang ở màn hình gốc.
Hành vi của nút Quay lại đã tiến hóa qua từng phiên bản Android. Android 10 giới thiệu điều hướng bằng cử chỉ, Android 11 giới thiệu OnBackPressedDispatcher như API tiêu chuẩn, và Android 13 mang lại hỗ trợ cải thiện cho predictive back gesture, nơi hệ thống hiển thị hoạt ảnh chuyển tiếp trước khi hành động thực sự được thực thi. Google đang nhất quán hướng tới hành vi Back Press có thể dự đoán và nhất quán trên tất cả các thiết bị.
Xử lý đúng nút Quay lại là một yếu tố UX quan trọng trong ứng dụng Android. Người dùng mong đợi nhấn Quay lại sẽ đưa họ về màn hình trước trong ngăn xếp điều hướng, chứ không phải đóng ứng dụng một cách bất ngờ. Vi phạm kỳ vọng này là một trong những nguyên nhân chính dẫn đến đánh giá tiêu cực và xếp hạng thấp trên Google Play.
Lịch sử của Back Press API trong Android phản ánh sự tiến hóa chung của nền tảng: từ một phương thức đơn giản trong Activity đến một hệ thống callback linh hoạt hỗ trợ vòng đời và Compose. Hãy xem xét ba giai đoạn phát triển.
Ngay từ API Android đầu tiên, nút Quay lại được xử lý trong phương thức onBackPressed của lớp Activity. Nhà phát triển ghi đè phương thức này và viết logic riêng của mình. Vấn đề là Fragment và View không thể chặn nhấn nút — tất cả điều khiển đều thông qua Activity. Điều này dẫn đến Activity phình to và chuỗi if-else phức tạp để xác định ai nên xử lý nhấn nút.
Với Activity 1.0.0 (AndroidX), Google đã giới thiệu OnBackPressedDispatcher. Đây là một bộ điều phối trung tâm chấp nhận callback từ bất kỳ thành phần nào — Activity, Fragment, Dialog, View tùy chỉnh. Các callback được đăng ký theo thứ tự (thông qua priority) và có thể được thêm hoặc xóa động. OnBackPressedDispatcher được gọi trước onBackPressed cũ, cho phép chặn nhấn nút trước khi Activity xử lý.
Android 13 đã giới thiệu predictive back gesture — hoạt ảnh hệ thống hiển thị nơi nhấn Quay lại sẽ dẫn đến trước khi người dùng hoàn thành cử chỉ. Để hỗ trợ hoạt ảnh này, các nhà phát triển phải sử dụng OnBackPressedDispatcher và chỉ định liệu callback có hỗ trợ hoạt ảnh hệ thống thông qua thuộc tính isEnabled hay không. Nếu callback không hỗ trợ predictive animation, hệ thống hiển thị hoạt ảnh mặc định có thể không khớp với ngữ cảnh ứng dụng.
| API | SDK tối thiểu | Hỗ trợ Fragment | Predictive Back |
|---|---|---|---|
| onBackPressed | API Level 1 | Qua Activity | Không |
| OnBackPressedDispatcher | Activity 1.0.0 | Trực tiếp | Một phần |
| OnBackPressedDispatcher + lifecycle | Activity 1.3.0 | Lifecycle-aware | Đầy đủ |
OnBackPressedDispatcher là cốt lõi của API Back Press mới. Nó quản lý một chuỗi các callback, gọi chúng theo thứ tự cho đến khi callback đầu tiên xử lý sự kiện. Nếu không có callback nào xử lý nhấn nút, bộ điều phối thực thi hành động mặc định — gọi finish() cho Activity hoặc popBackStack() cho Navigation Component.
Một callback được đăng ký qua addCallback với LifecycleOwner và đối tượng OnBackPressedCallback. Callback có thuộc tính isEnabled — nếu được đặt thành false, callback sẽ bị bỏ qua. Đối với ưu tiên, bạn có thể truyền giá trị từ 0 (thấp nhất) đến Integer.MAX_VALUE. Fragment Activity Result API sử dụng cơ chế này để tự động đăng ký callback gắn với vòng đời.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
if (isDrawerOpen) { closeDrawer() }
else { isEnabled = false; onBackPressed() }
}
}
onBackPressedDispatcher.addCallback(this, callback)
}
Callback được tự động xóa khi LifecycleOwner chuyển sang trạng thái DESTROYED. Điều này giải quyết vấn đề rò rỉ callback cũ khi xoay màn hình. Nếu callback được thêm trong Fragment, nó được đảm bảo sẽ bị xóa khi Fragment bị hủy. Để tạm thời vô hiệu hóa callback, hãy sử dụng thuộc tính isEnabled — bạn có thể chuyển đổi mà không cần xóa và đăng ký lại.
Thứ tự gọi ngược với thứ tự thêm: callback được thêm cuối cùng nhận quyền điều khiển trước. Điều này hợp lý vì phần tử UI lồng nhau nhất (ví dụ: Bottom Sheet trong Fragment) nên xử lý nhấn nút trước Fragment cha của nó. Nếu callback sâu nhất không xử lý nhấn nút (isEnabled = false), quyền điều khiển sẽ chuyển đến callback tiếp theo trong chuỗi.
Fragment API cung cấp tích hợp riêng với OnBackPressedDispatcher thông qua phương thức requireActivity().onBackPressedDispatcher. Bắt đầu từ Fragment 1.2.0, mỗi Fragment có thể đăng ký callback riêng của mình, tự động gắn với vòng đời của Fragment và bị xóa khi nó bị hủy.
Đăng ký callback trong Fragment được thực hiện trong onCreate, onViewCreated hoặc thậm chí trong chính View — điều quan trọng là LifecycleOwner (Fragment) đang hoạt động. Khi Fragment chuyển sang trạng thái STARTED, callback được kích hoạt; khi STOPPED, nó bị vô hiệu hóa. Điều này đảm bảo rằng một Fragment ẩn (trong ViewPager) sẽ không xử lý nhấn Quay lại.
class EditorFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showDiscardDialog()
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
}
Hộp thoại và BottomSheets tự động chặn nhấn Quay lại để đóng. Nếu bạn cần thực hiện hành động bổ sung trước khi đóng — hãy đăng ký callback với ưu tiên cao hơn. Quan trọng: nếu bạn đặt setCancelable(false) trên Dialog, callback sẽ không kích hoạt — đây là hành vi hệ thống.
Jetpack Compose cung cấp API khai báo để xử lý nút Quay lại thông qua hàm composable BackHandler. BackHandler chấp nhận enabled (trạng thái boolean) và onBack — callback được gọi khi nhấn nút. Nếu enabled = false, nhấn nút được chuyển tiếp xuống chuỗi.
BackHandler tự động đăng ký OnBackPressedCallback trong OnBackPressedDispatcher của Activity cha. Nó tôn trọng vòng đời của composable: khi rời khỏi thành phần, callback sẽ bị xóa. enabled có thể được gắn với trạng thái — ví dụ: chỉ hiển thị hộp thoại xác nhận nếu biểu mẫu có thay đổi chưa được lưu.
@Composable
fun EditScreen(hasUnsavedChanges: Boolean) {
BackHandler(enabled = hasUnsavedChanges) {
// Show confirmation dialog
}
Column {
TextField(value = ..., onValueChange = ...)
}
}
Predictive back gesture trong Compose được hỗ trợ từ Compose 1.5.0. BackHandler tự động xử lý hoạt ảnh chuyển tiếp hệ thống nếu nó được bật trên thiết bị. Đối với hoạt ảnh predictive tùy chỉnh, hãy sử dụng bổ từ predictiveBackHandler, trả về tiến trình cử chỉ từ 0 đến 1.
Back Press Handling có vẻ đơn giản, nhưng trong thực tế, các nhà phát triển mắc một số lỗi hệ thống. Hãy xem xét các vấn đề phổ biến nhất và giải pháp của chúng dựa trên khuyến nghị của Google và kinh nghiệm cộng đồng.
Gọi trực tiếp finish() trong handleOnBackPressed có thể dẫn đến thoát ứng dụng bất ngờ nếu có màn hình nền trong ngăn xếp điều hướng. Luôn kiểm tra NavController.backStack thông qua Navigation Component hoặc Coordinator trước khi đóng Activity.
Nếu bạn đăng ký callback mà không có LifecycleOwner (sử dụng addCallback cũ không có tham số), callback sẽ tồn tại mãi mãi và có thể gây ra NullPointerException nếu Activity đã bị hủy. Luôn sử dụng addCallback(this, callback) với LifecycleOwner.
Đối với cửa sổ phương thức (Bottom Sheet, Dialog), luôn đặt isEnabled = true chỉ khi cửa sổ hiển thị. Sử dụng addCallback với lambda kiểm tra trạng thái cửa sổ. Navigation Component tự động quản lý điều này cho NavHost.
Nhấn đúp nhanh Quay lại có thể dẫn đến gọi đúp finish(). Sử dụng cờ hoặc throttleLast để bảo vệ khỏi các cuộc gọi lặp lại trong vòng 500 ms. Navigation Component xử lý tình huống này một cách tự nhiên, nhưng trong các tình huống tùy chỉnh, bạn cần triển khai bảo vệ thủ công.
private var lastBackPressTime = 0L
override fun handleOnBackPressed() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastBackPressTime > 500) {
lastBackPressTime = currentTime
navigateBack()
}
}
Câu hỏi thường gặp
onBackPressed bị lỗi thời trong Android 11 vì nó chỉ hoạt động ở cấp Activity. OnBackPressedDispatcher cho phép bất kỳ thành phần nào (Fragment, Dialog, View) chặn nhấn nút thông qua một cơ chế duy nhất hỗ trợ vòng đời.
Hệ thống Android chuyển đổi cử chỉ vuốt từ cạnh thành nhấn Quay lại hệ thống trước khi ứng dụng nhận được nó. Ở cấp OnBackPressedDispatcher, bạn không thể phân biệt hai sự kiện này — cả hai đều đến dưới dạng handleOnBackPressed.
Có, xử lý OnBackPressedDispatcher giống nhau cho thiết bị ba nút và điều hướng bằng cử chỉ. Mã chặn không phụ thuộc vào loại điều hướng — hệ thống tự chuyển đổi cử chỉ thành cuộc gọi bộ điều phối.
Bật predictive back trong Developer Options của trình giả lập Android 13+. Sử dụng ADB: `adb shell settings put global enable_back_animation 1`. Sau khi bật, hoạt ảnh hệ thống sẽ hiển thị xem trước chuyển tiếp khi nhấn Quay lại.
Kiểm tra hai điều kiện: LifecycleOwner phải ở trạng thái STARTED hoặc RESUMED, và isEnabled của callback phải là true. Nếu cả hai điều kiện được đáp ứng, hãy đảm bảo callback được thêm vào OnBackPressedDispatcher chính xác — sử dụng requireActivity().onBackPressedDispatcher trong Fragment.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm