Думаю, всі розробники стикалися з проблемою багаторазового натискання кнопок користувачем. Особливий біль приносили кнопки, до яких була прикріплена навігація.
Раніше, повторне натискання просто клало застосунок, так як процес уже запустився, і ще раз навігуватись із поточного екрану було не можливо.
Зараз саме цю проблему пофіксили, і апка не вилітає, проте екран створюється кілька разів (по кількості натискань) і потрапляє в стек навігації, що теж не є добре.
Звісно, розробники вигадали купу різних способів, щоб це обійти. І затримка часу, коли на деякий час після натискання кнопка блокується. Блокування кнопки після натискання, обертання навігації в LaunchEffect, перевірка currentDestination та інші цікаві способи.
Це все працювало, але це все "милиці", якими підпирали проблему.
І ось, не знаю коли саме знайшлось це рішення, але воно є:
Це функція dropUnlessResumed()
Button(
onClick =
dropUnlessResumed {
// Run on clicks only when the lifecycle is at least RESUMED.
// Example: navController.navigate("next_screen")
},
) {
Text(text = "Navigate to next screen")
}
Ось так вона виглядає:
@Composable
fun dropUnlessResumed(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
block: () -> Unit
): () -> Unit
Цей функція блокує виконання коду, що в ній розміщено, якщо
lifecycleState != State.RESUMED
От ми і підійшли до моменту, коли потрібно розкрити роль Lifesycle у всьому цьому.
Деякі розробники сумніваються в правильності підходу використання функції dropUnlessResumed(), хоча саме такий спосіб її використання вказаний у документації. Аргументують це тим, що стан життєвого циклу можуть змінити і інші чинники.
То ж, давайте трохи розберемось із цим станом
Екрани написані з допомогою compose мають свій життєвий цикл:
CREATED
STARTED
RESUMED
DESTROYED
Доступний для користування екран, має стан RESUMED. В момент натискання кнопки і виклику навігації, система генерує подію, яка приводить до зміни стану за таким принципом:
ON_CREATE, ON_STOP -> return State.CREATED
ON_START, ON_PAUSE -> return State.STARTED
ON_RESUME -> return State.RESUMED
ON_DESTROY -> return State.DESTROYED
ON_ANY -> {}
В нашому випадку це ON_PAUSE і STARTED, відповідно.
Оскільки STARTED не RESUMED - блок коду, що запускає навігацію, який ми помістили у фігурні дужки методу dropUnlessResumed не буде виконано, скільки б разів не тиснули на кнопку.
Тепер щодо зміни стану з інших причин. Функція блокує, а не запускає виконання коду. То ж, якщо стан життєвого циклу зміниться через інші події, дія кнопки буде заблокована. Але ж нам це і потрібно, чи не так?