Permission Handler — це компонент Android-застосунку, що відповідає за перевірку, запит та обробку результатів runtime-дозволів. Згідно з Android Developer Guide, 2024, обробник дозволів централізує логіку checkSelfPermission, requestPermissions та shouldShowRequestPermissionRationale в єдиному класі або ViewModel. Це спрощує підтримку коду та покращує тестування.
Головне
Permission Handler — це архітектурний патерн для централізованого керування runtime-дозволами Android. Замість розрізнених викликів ContextCompat.checkSelfPermission та ActivityCompat.requestPermissions по всьому коду застосунку, вся логіка запитів та обробки результатів зосереджується в одному класі. Це зменшує дублювання, спрощує підтримку та робить код більш передбачуваним.
Необхідність у Permission Handler виникла з впровадженням runtime-дозволів в Android 6.0. До цього всі дозволи запитувалися під час встановлення, і код застосунку міг використовувати будь-які API без перевірок. Після переходу на runtime-модель кожне використання небезпечного дозволу вимагає триетапної перевірки: checkSelfPermission, requestPermissions, onRequestPermissionsResult. Розмазування цієї логіки по Activity та Fragment призводить до інлайн-дублювання та помилок. За даними Google I/O 2019, централізація обробки дозволів знижує кількість багів, пов'язаних з Permission Denial, в середньому на 60 відсотків.
Хороший Permission Handler надає чистий інтерфейс для коду, що викликає. Activity або Fragment не повинні знати деталі запиту — вони викликають метод типу requestCamera(callback), а handler сам керує перевіркою статусу, відображенням rationale, викликом системного діалогу та передачею результату в callback. Це реалізує принцип єдиної відповідальності та відокремлює бізнес-логіку від платформеного коду дозволів.
Permission Handler стає необхідним, коли застосунок використовує 3 і більше небезпечних дозволів. Для простих застосунків з одним дозволом (наприклад, камера для сканера QR-кодів) можна обійтися прямим викликом. Але для типового мобільного застосунку з камерою, геолокацією, сповіщеннями та сховищем — централізований handler обов'язковий для підтримки.
Типовий Permission Handler складається з трьох рівнів: інтерфейс-контракт, реалізація з ActivityResultLauncher та рівень для ViewModel. Інтерфейс визначає методи запиту для кожного дозволу — requestCamera, requestLocation, requestStorage. Реалізація пов'язує ці методи з відповідними контрактами ActivityResultContracts.RequestPermission.
Ключові компоненти архітектури:
Така архітектура дозволяє легко підміняти реалізацію в тестах: замість реального ActivityResultLauncher використовується мок, який повертає попередньо визначений результат без взаємодії з системою. Це критично важливо для юніт-тестування UI-логіки, де запуск Activity для діалогу дозволів неможливий.
Permission Handler повинен враховувати життєвий цикл Activity та Fragment. Лаунчери реєструються в ActivityResultRegistry, який автоматично зберігає та відновлює стан при повороті екрану та перестворенні Activity. Handler не повинен зберігати прямі посилання на Activity або Fragment — натомість використовуйте WeakReference або передавайте registry через конструктор. Це запобігає витокам пам'яті та крашам при configuration changes.
Базова реалізація Permission Handler будується на ActivityResultContracts.RequestPermission. Handler отримує ActivityResultRegistry з ComponentActivity або Fragment та реєструє лаунчери для кожного дозволу. Кожен лаунчер приймає лямбду-колбек, яка викликається після відповіді користувача.
sealed class PermissionResult {
object GRANTED : PermissionResult()
data class DENIED(
val shouldShowRationale: Boolean
) : PermissionResult()
}
interface PermissionHandler {
fun requestCamera(
callback: (PermissionResult) -> Unit
)
fun requestLocation(
callback: (PermissionResult) -> Unit
)
fun isPermissionGranted(
permission: String
): Boolean
}
class AndroidPermissionHandler(
private val registry: ActivityResultRegistry,
private val context: Context
) : PermissionHandler {
private var cameraLauncher: ActivityResultLauncher<String>? = null
fun initialize() {
cameraLauncher = registry.register(
"camera_permission",
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
pendingCameraCallback?.invoke(
PermissionResult.GRANTED
)
} else {
val rationale = ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
Manifest.permission.CAMERA
)
pendingCameraCallback?.invoke(
PermissionResult.DENIED(rationale)
)
}
}
}
private var pendingCameraCallback:
((PermissionResult) -> Unit)? = null
override fun requestCamera(
callback: (PermissionResult) -> Unit
) {
if (isPermissionGranted(
Manifest.permission.CAMERA
)) {
callback.invoke(PermissionResult.GRANTED)
return
}
pendingCameraCallback = callback
cameraLauncher?.launch(
Manifest.permission.CAMERA
)
}
override fun isPermissionGranted(
permission: String
): Boolean {
return ContextCompat.checkSelfPermission(
context, permission
) == PackageManager.PERMISSION_GRANTED
}
}
Handler ініціалізується в onCreate Activity через registerForActivityResult, який надає доступ до ActivityResultRegistry. Після ініціалізації handler готовий обробляти запити протягом всього життєвого циклу Activity. Важливо викликати initialize до першого запиту, інакше лаунчер не буде зареєстровано.
Інтеграція Permission Handler з ViewModel — найбільш просунутий підхід. ViewModel керує станом запитів, а Handler виконує лише платформені виклики. ViewModel містить StateFlow<PermissionUiState>, де UiState описує, який дозвіл запитується та який результат отримано. Activity підписується на цей StateFlow та делегує запит Handler.
class PermissionsViewModel : ViewModel() {
private val _uiState =
MutableStateFlow<PermissionUiState>(
PermissionUiState.Idle
)
val uiState: StateFlow<PermissionUiState> = _uiState.asStateFlow()
fun onCameraRequested() {
_uiState.value = PermissionUiState.RequestingCamera
}
fun onPermissionResult(
permission: String,
result: PermissionResult
) {
when (result) {
PermissionResult.GRANTED -> {
_uiState.value = PermissionUiState.Granted(permission)
}
is PermissionResult.DENIED -> {
_uiState.value = PermissionUiState.Denied(
permission,
result.shouldShowRationale
)
}
}
}
}
sealed class PermissionUiState {
object Idle : PermissionUiState()
object RequestingCamera : PermissionUiState()
data class Granted(val permission: String) : PermissionUiState()
data class Denied(
val permission: String,
val shouldShowRationale: Boolean
) : PermissionUiState()
}
У цій моделі Activity перевіряє isPermissionGranted через Handler при старті, а ViewModel керує лише станом. Якщо дозвіл не надано — Activity підписується на uiState, викликає requestCamera з Handler і передає результат назад у ViewModel через onPermissionResult. Розділення платформеного коду та бізнес-логіки дозволяє тестувати ViewModel без Android-залежностей.
Юніт-тестування Permission Handler можливе завдяки інтерфейсу PermissionHandler. У тестах створюється FakePermissionHandler, який імітує різні сценарії: дозвіл надано, відхилено, Never Ask Again. Кожен сценарій перевіряється незалежно. Це особливо важливо для тестування UI-логіки, яка повинна коректно реагувати на всі три результати.
class FakePermissionHandler : PermissionHandler {
var cameraResult: PermissionResult =
PermissionResult.GRANTED
var grantedPermissions: Set<String> =
setOf(Manifest.permission.CAMERA)
override fun requestCamera(
callback: (PermissionResult) -> Unit
) {
callback.invoke(cameraResult)
}
override fun isPermissionGranted(
permission: String
): Boolean {
return permission in grantedPermissions
}
}
Fake-реалізація дозволяє тестувати ViewModel без емулятора. Достатньо встановити cameraResult у потрібне значення та перевірити, що ViewModel коректно оновлює UiState. Інтеграційні тести перевіряють реальний PermissionHandler з ActivityScenario, але таких тестів зазвичай 2-3 на весь застосунок — решта сценаріїв покриваються юніт-тестами з фейками.
Типові помилки при роботі з Permission Handler включають: відсутність перевірки checkSelfPermission перед кожним викликом API, ігнорування shouldShowRequestPermissionRationale, повторний виклик requestPermissions при Never Ask Again та зберігання лаунчерів без урахування життєвого циклу Activity. Розглянемо кожну проблему та рішення.
Найпоширеніша помилка — виклик API без перевірки статусу дозволу. Розробники припускають, що якщо дозвіл було надано один раз, він залишиться назавжди. Однак користувач може відкликати його через налаштування в будь-який момент. Permission Handler повинен завжди викликати isPermissionGranted перед виконанням чутливої операції. Друга популярна помилка — ігнорування shouldShowRequestPermissionRationale та повторний запит, який призводить до миттєвої відмови без діалогу при Never Ask Again.
Кращі практики включають: створення одного екземпляра Handler на весь життєвий цикл Activity, використання SharedFlow для передачі результатів у ViewModel, логування всіх запитів та відмов для аналітики, а також показ кастомного rationale-діалогу перед системним при першій відмові. Дотримання цих правил гарантує стабільну роботу з дозволами на всіх версіях Android.
Поширені запитання
Permission Handler — це компонент для централізованого керування runtime-дозволами, що інкапсулює checkSelfPermission, requestPermissions та shouldShowRequestPermissionRationale. Він спрощує підтримку коду та покращує тестування.
Рекомендується ActivityResultContracts.RequestPermission з бібліотеки androidx.activity. Він замінює застарілий onRequestPermissionsResult та надає чистий callback API з результатом Boolean.
Для одного дозволу Handler не обов'язковий — можна використовувати прямий виклик RequestPermission лаунчера в Activity. Handler стає необхідним при 3 і більше дозволах, щоб уникнути дублювання коду.
Створіть інтерфейс PermissionHandler та його fake-реалізацію для юніт-тестів. Fake повертає попередньо визначені результати без системних викликів. Це дозволяє тестувати ViewModel та UI-логіку без емулятора.
Після відмови перевірте shouldShowRequestPermissionRationale. Якщо метод повернув false — активовано режим Never Ask Again. Handler повинен повернути PermissionResult.DENIED(false), а UI — показати кнопку переходу в Settings.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також