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 через конструктор. Это предотвращает утечки памяти и crash при 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также