Back Press Handling — е механизмът за прихващане и обработка на системния бутон „Назад” в Android, който определя какво действие се изпълнява при натискането му. Според Android Developers (2024), от Android 11 традиционният метод onBackPressed() е заменен от OnBackPressedDispatcher. Новото API позволява на компоненти на всяко ниво от йерархията да прихващат натискането, а не само на Activity. Ключова разлика — поддръжка на множество callback във верига с приоритети.
Основни положения
Back Press Handling — е системният механизъм на Android, който определя какво се случва, когато потребителят натисне хардуерния или програмния бутон „Назад”. В зависимост от контекста натискането може: да затвори текущия екран и да се върне към предишния, да скрие клавиатурата, да затвори Drawer или Bottom Sheet, да излезе от приложението, ако потребителят е на началния екран.
Поведението на бутона „Назад” еволюира с всяка версия на Android. В Android 10 се появиха навигационни жестове, в Android 11 — OnBackPressedDispatcher като стандартен API, а в Android 13 — подобрена поддръжка на predictive back gesture, където системата показва анимация на прехода преди действителното изпълнение на действието. Google последователно се движи към предвидимо и консистентно поведение на Back Press на всички устройства.
Правилната обработка на бутона „Назад” — критичен елемент на UX в Android приложението. Потребителят очаква, че натискането на „Назад” го връща на предишния екран в стека за навигация, а не затваря приложението неочаквано. Нарушаването на това очакване е една от основните причини за отрицателни отзиви и ниски оценки в Google Play.
Историята на Back Press API в Android отразява общата еволюция на платформата: от прост метод в Activity до гъвкава система от callback с поддръжка на lifecycle и Compose. Нека разгледаме три етапа на развитие.
От самото начало на Android API бутонът „Назад” се обработваше в метода onBackPressed на класа Activity. Разработчикът презаписваше този метод и пишеше своя логика. Проблемът беше, че Fragment и View не можеха да прихванат натискането — цялото управление вървеше през Activity. Това водеше до надути Activity и сложна верига if-else за определяне кой трябва да обработи натискането.
С Activity 1.0.0 (AndroidX) Google представи OnBackPressedDispatcher. Това е централен диспечер, който приема callback от всеки компонент — Activity, Fragment, Dialog, персонализиран View. Callback-овете се регистрират с посочване на ред (чрез priority) и могат да се добавят или премахват динамично. OnBackPressedDispatcher се извиква по-рано от стария onBackPressed, което позволява прихващане на натискането преди Activity да го обработи.
Android 13 въведе predictive back gesture — системна анимация, която показва накъде води натискането на „Назад”, още преди потребителят да завърши жеста. За поддръжка на тази анимация разработчиците трябва да използват OnBackPressedDispatcher и да посочат дали callback-ът поддържа системната анимация чрез свойството isEnabled. Ако callback-ът не поддържа predictive animation, системата показва анимация по подразбиране, която може да не съответства на контекста на приложението.
| API | Минимален SDK | Поддръжка на Fragment | Predictive Back |
|---|---|---|---|
| onBackPressed | API Level 1 | Чрез Activity | Не |
| OnBackPressedDispatcher | Activity 1.0.0 | Директна | Частична |
| OnBackPressedDispatcher + lifecycle | Activity 1.3.0 | Lifecycle-aware | Пълна |
OnBackPressedDispatcher — е ядрото на новото Back Press API. Той управлява веригата от callback, извиквайки ги последователно до първия, който обработи събитието. Ако никой callback не обработи натискането, диспечерът изпълнява действието по подразбиране — извикване на finish() за Activity или popBackStack() за Navigation Component.
Callback се регистрира чрез addCallback с посочване на LifecycleOwner и обект OnBackPressedCallback. Callback-ът съдържа свойството isEnabled — ако е зададено на false, callback-ът се пропуска. За priority може да се предаде стойност от 0 (най-нисък) до Integer.MAX_VALUE. Fragment Activity Result API използва този механизъм за автоматична регистрация на callback, свързани с жизнения цикъл.
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-ът се премахва автоматично, когато LifecycleOwner премине в състояние DESTROYED. Това решава стария проблем с изтичане на callback при завъртане на екрана. Ако callback е добавен във Fragment, гарантирано ще бъде премахнат при унищожаване на Fragment. За временно деактивиране на callback се използва свойството isEnabled — може да се превключва без премахване и повторна регистрация.
Редът на извикване е обратен на реда на добавяне: последният добавен callback получава контрол първи. Това е логично, защото най-вложеният UI елемент (например Bottom Sheet вътре във Fragment) трябва да обработи натискането по-рано от родителския Fragment. Ако най-дълбокият callback не обработва натискането (isEnabled = false), контролът се предава на следващия във веригата.
Fragment API предоставя собствена интеграция с OnBackPressedDispatcher чрез метода requireActivity().onBackPressedDispatcher. От Fragment 1.2.0 всеки Fragment може да регистрира свой собствен callback, който автоматично се свързва с жизнения цикъл на Fragment и се премахва при неговото унищожаване.
Регистрацията на callback във Fragment се извършва в onCreate, onViewCreated или дори в самия View — важно е само LifecycleOwner (Fragment) да е активен. Когато Fragment премине в състояние STARTED, callback-ът се включва, при STOPPED — се изключва. Това гарантира, че скрит Fragment (в ViewPager) няма да обработва натискането на „Назад”.
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)
}
}
Диалоговите прозорци и BottomSheet автоматично прихващат натискането на „Назад” за затваряне. Ако трябва да изпълните допълнително действие преди затваряне — регистрирайте callback с по-висок приоритет. Важно: ако сте задали setCancelable(false) на Dialog, callback-ът няма да работи — това е системно поведение.
Jetpack Compose предоставя декларативно API за обработка на бутона „Назад” чрез composable функцията BackHandler. BackHandler приема enabled (булева стойност) и onBack — callback, който се извиква при натискане. Ако enabled = false, натискането се предава по-нататък във веригата.
BackHandler автоматично регистрира OnBackPressedCallback в OnBackPressedDispatcher на родителското Activity. Той взема предвид жизнения цикъл на composable: при излизане от композицията callback-ът се премахва. enabled може да бъде свързан със състояние — например да показва диалогов прозорец за потвърждение само ако има незапазени промени във формата.
@Composable
fun EditScreen(hasUnsavedChanges: Boolean) {
BackHandler(enabled = hasUnsavedChanges) {
// Показване на диалогов прозорец за потвърждение
}
Column {
TextField(value = ..., onValueChange = ...)
}
}
Predictive back gesture в Compose се поддържа от Compose 1.5.0. BackHandler автоматично обработва системната анимация на прехода, ако е активирана на устройството. За персонализирана predictive анимация използвайте модификатора predictiveBackHandler, който връща напредъка на жеста от 0 до 1.
Back Press Handling изглежда прост, но на практика разработчиците допускат редица систематични грешки. Нека разгледаме най-честите проблеми и начините за тяхното решаване въз основа на препоръките на Google и опита на общността.
Директното извикване на finish() в handleOnBackPressed може да доведе до неочаквано излизане от приложението, ако в стека за навигация има фонови екрани. Винаги проверявайте NavController.backStack чрез Navigation Component или Coordinator преди затваряне на Activity.
Ако регистрирате callback без LifecycleOwner (използвайки стария addCallback без параметър), callback-ът ще живее вечно и може да причини NullPointerException, ако Activity вече е унищожено. Винаги използвайте addCallback(this, callback) с LifecycleOwner.
За модални прозорци (Bottom Sheet, Dialog) винаги задавайте isEnabled = true само когато прозорецът е видим. Използвайте addCallback с ламбда, която проверява състоянието на прозореца. Navigation Component автоматично управлява това за NavHost.
Бързото двойно натискане на „Назад” може да доведе до двойно извикване на finish(). Използвайте флаг или throttleLast за защита от повторни извиквания в рамките на 500 ms. Navigation Component вградено обработва тази ситуация, но в персонализирани сценарии трябва да имплементирате защитата ръчно.
private var lastBackPressTime = 0L
override fun handleOnBackPressed() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastBackPressTime > 500) {
lastBackPressTime = currentTime
navigateBack()
}
}
Често задавани въпроси
onBackPressed стана остарял (deprecated) в Android 11, защото работи само на ниво Activity. OnBackPressedDispatcher позволява на всеки компонент (Fragment, Dialog, View) да прихваща натискането чрез единен механизъм с поддръжка на жизнен цикъл.
Системата Android преобразува жеста на плъзгане от ръба в системно натискане на „Назад”, преди приложението да го получи. На ниво OnBackPressedDispatcher не можете да различите тези две събития — и двете идват като handleOnBackPressed.
Да, обработката на OnBackPressedDispatcher е еднаква за устройства с три бутона и с жестова навигация. Кодът за прихващане не зависи от типа навигация — системата сама преобразува жеста в извикване на диспечера.
Включете predictive back в Developer Options на емулатора с Android 13+. Използвайте ADB: `adb shell settings put global enable_back_animation 1`. След включване системната анимация ще покаже преглед на прехода при натискане на „Назад”.
Проверете две условия: LifecycleOwner трябва да е в състояние STARTED или RESUMED, и isEnabled на callback-а трябва да е true. Ако и двете условия са изпълнени, уверете се, че callback-ът е добавен към правилния OnBackPressedDispatcher — използвайте requireActivity().onBackPressedDispatcher във Fragment.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също