Permission Handler is an Android application component responsible for checking, requesting and processing the results of runtime permissions. According to the Android Developer Guide, 2024, a permission handler centralizes the logic of checkSelfPermission, requestPermissions and shouldShowRequestPermissionRationale in a single class or ViewModel. This simplifies code maintenance and improves testing.
Key Takeaways
Permission Handler is an architectural pattern for centralized management of Android runtime permissions. Instead of scattered calls to ContextCompat.checkSelfPermission and ActivityCompat.requestPermissions throughout the application code, all request and result processing logic is concentrated in a single class. This reduces duplication, simplifies maintenance and makes the code more predictable.
The need for Permission Handler arose with the introduction of runtime permissions in Android 6.0. Before that, all permissions were requested at install time, and the application code could use any APIs without checks. After switching to the runtime model, each use of a dangerous permission requires a three-step check: checkSelfPermission, requestPermissions, onRequestPermissionsResult. Spreading this logic across Activity and Fragment leads to inline duplication and errors. According to Google I/O 2019, centralizing permission handling reduces the number of Permission Denial-related bugs by an average of 60 percent.
A good Permission Handler provides a clean interface for the calling code. Activity or Fragment should not know the details of the request — they call a method like requestCamera(callback), and the handler itself manages status checking, rationale display, system dialog invocation and passing the result to the callback. This implements the single responsibility principle and separates business logic from platform permission code.
A Permission Handler becomes necessary when an application uses 3 or more dangerous permissions. For simple applications with one permission (e.g., camera for a QR code scanner), a direct call may suffice. But for a typical mobile application with camera, geolocation, notifications and storage — a centralized handler is essential for maintainability.
A typical Permission Handler consists of three layers: an interface contract, an implementation with ActivityResultLauncher, and a ViewModel layer. The interface defines request methods for each permission — requestCamera, requestLocation, requestStorage. The implementation binds these methods to the corresponding ActivityResultContracts.RequestPermission contracts.
Key architecture components:
This architecture makes it easy to swap implementations in tests: instead of a real ActivityResultLauncher, a mock that returns a predefined result without system interaction is used. This is critical for unit testing UI logic, where launching an Activity for a permission dialog is impossible.
The Permission Handler must take into account the Activity and Fragment lifecycle. Launchers are registered in the ActivityResultRegistry, which automatically saves and restores state on screen rotation and Activity recreation. The handler should not store direct references to Activity or Fragment — instead, use WeakReference or pass the registry through the constructor. This prevents memory leaks and crashes during configuration changes.
A basic implementation of a Permission Handler is built on ActivityResultContracts.RequestPermission. The handler receives the ActivityResultRegistry from ComponentActivity or Fragment and registers launchers for each permission. Each launcher accepts a callback lambda that is invoked after the user responds.
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
}
}
The handler is initialized in the Activity's onCreate via registerForActivityResult, which provides access to the ActivityResultRegistry. After initialization, the handler is ready to process requests throughout the Activity lifecycle. It is important to call initialize before the first request, otherwise the launcher will not be registered.
Integrating a Permission Handler with ViewModel is the most advanced approach. The ViewModel manages the request state, while the Handler only performs platform calls. The ViewModel contains StateFlow<PermissionUiState>, where UiState describes which permission is being requested and what result was received. The Activity subscribes to this StateFlow and delegates the request to the 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()
}
In this model, the Activity checks isPermissionGranted through the Handler at startup, while the ViewModel only manages state. If the permission is not granted — the Activity subscribes to uiState, calls requestCamera from the Handler and passes the result back to the ViewModel via onPermissionResult. Separating platform code from business logic allows testing the ViewModel without Android dependencies.
Unit testing a Permission Handler is possible thanks to the PermissionHandler interface. In tests, a FakePermissionHandler is created that simulates various scenarios: permission granted, denied, Never Ask Again. Each scenario is tested independently. This is especially important for testing UI logic that must correctly react to all three outcomes.
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
}
}
The fake implementation allows testing ViewModel without an emulator. Simply set cameraResult to the desired value and verify that the ViewModel correctly updates its UiState. Integration tests check the real PermissionHandler with ActivityScenario, but typically there are only 2-3 such tests per application — the remaining scenarios are covered by unit tests with fakes.
Typical mistakes when working with Permission Handler include: not checking checkSelfPermission before each API call, ignoring shouldShowRequestPermissionRationale, calling requestPermissions again after Never Ask Again, and storing launchers without considering the Activity lifecycle. Let us examine each problem and its solution.
The most common mistake is calling an API without checking the permission status. Developers assume that if a permission was granted once, it will remain forever. However, the user can revoke it through settings at any time. A Permission Handler should always call isPermissionGranted before performing a sensitive operation. The second popular mistake is ignoring shouldShowRequestPermissionRationale and repeating the request, which leads to an instant denial without a dialog under Never Ask Again.
Best practices include: creating a single Handler instance for the entire Activity lifecycle, using SharedFlow to pass results to the ViewModel, logging all requests and denials for analytics, and showing a custom rationale dialog before the system one on the first denial. Following these rules guarantees stable permission handling across all Android versions.
Frequently Asked Questions
Permission Handler is a component for centralized management of runtime permissions, encapsulating checkSelfPermission, requestPermissions and shouldShowRequestPermissionRationale. It simplifies code maintenance and improves testing.
It is recommended to use ActivityResultContracts.RequestPermission from the androidx.activity library. It replaces the deprecated onRequestPermissionsResult and provides a clean callback API with a Boolean result.
For a single permission, a Handler is not mandatory — you can use a direct RequestPermission launcher call in the Activity. A Handler becomes necessary with 3 or more permissions to avoid code duplication.
Create a PermissionHandler interface and its fake implementation for unit tests. The fake returns predefined results without system calls. This allows testing ViewModel and UI logic without an emulator.
After denial, check shouldShowRequestPermissionRationale. If the method returns false — Never Ask Again mode is active. The Handler should return PermissionResult.DENIED(false), and the UI should show a button to navigate to Settings.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also