Permission Handler to komponent aplikacji Android odpowiedzialny za sprawdzanie, żądanie i obsługę wyników uprawnień runtime. Według Android Developer Guide, 2024, procedura obsługi uprawnień centralizuje logikę checkSelfPermission, requestPermissions i shouldShowRequestPermissionRationale w jednej klasie lub ViewModel. Upraszcza to utrzymanie kodu i poprawia testowanie.
Najważniejsze
Permission Handler — to wzorzec architektoniczny do scentralizowanego zarządzania uprawnieniami runtime w Android. Zamiast rozproszonych wywołań ContextCompat.checkSelfPermission i ActivityCompat.requestPermissions w całym kodzie aplikacji, cała logika żądań i obsługi wyników koncentruje się w jednej klasie. Zmniejsza to powielanie, upraszcza utrzymanie i czyni kod bardziej przewidywalnym.
Potrzeba Permission Handler powstała wraz z wprowadzeniem uprawnień runtime w Android 6.0. Wcześniej wszystkie uprawnienia były żądane podczas instalacji, a kod aplikacji mógł korzystać z dowolnych API bez sprawdzania. Po przejściu na model runtime każde użycie niebezpiecznego uprawnienia wymaga trzystopniowej weryfikacji: checkSelfPermission, requestPermissions, onRequestPermissionsResult. Rozproszenie tej logiki pomiędzy Activity i Fragment prowadzi do powielania inline i błędów. Według danych Google I/O 2019, centralizacja obsługi uprawnień zmniejsza liczbę błędów związanych z Permission Denial średnio o 60 procent.
Dobry Permission Handler zapewnia czysty interfejs dla wywołującego kodu. Activity lub Fragment nie muszą znać szczegółów żądania — wywołują metodę typu requestCamera(callback), a handler sam zarządza sprawdzaniem statusu, wyświetlaniem rationale, wywołaniem systemowego dialogu i przekazaniem wyniku do callback. Realizuje to zasadę pojedynczej odpowiedzialności i oddziela logikę biznesową od kodu platformy uprawnień.
Permission Handler staje się niezbędny, gdy aplikacja używa 3 lub więcej niebezpiecznych uprawnień. W przypadku prostych aplikacji z jednym uprawnieniem (np. kamera do skanera kodów QR) można obejść się bezpośrednim wywołaniem. Jednak w typowej aplikacji mobilnej z kamerą, geolokalizacją, powiadomieniami i pamięcią masową — scentralizowany handler jest obowiązkowy dla utrzymania kodu.
Typowy Permission Handler składa się z trzech poziomów: interfejs-kontrakt, implementacja z ActivityResultLauncher i warstwa dla ViewModel. Interfejs określa metody żądań dla każdego uprawnienia — requestCamera, requestLocation, requestStorage. Implementacja łączy te metody z odpowiednimi kontraktami ActivityResultContracts.RequestPermission.
Kluczowe komponenty architektury:
Taka architektura pozwala łatwo podmieniać implementację w testach: zamiast rzeczywistego ActivityResultLauncher używany jest mock, który zwraca predefiniowany wynik bez interakcji z systemem. Jest to krytyczne dla testów jednostkowych logiki UI, gdzie uruchomienie Activity dla dialogu uprawnień jest niemożliwe.
Permission Handler musi uwzględniać cykl życia Activity i Fragment. Launchery są rejestrowane w ActivityResultRegistry, który automatycznie zapisuje i przywraca stan przy obrocie ekranu i odtworzeniu Activity. Handler nie powinien przechowywać bezpośrednich referencji do Activity lub Fragment — zamiast tego używaj WeakReference lub przekazuj registry przez konstruktor. Zapobiega to wyciekom pamięci i crashom przy zmianach konfiguracji.
Podstawowa implementacja Permission Handler opiera się na ActivityResultContracts.RequestPermission. Handler otrzymuje ActivityResultRegistry z ComponentActivity lub Fragment i rejestruje launchery dla każdego uprawnienia. Każdy launcher przyjmuje lambdę-callback, która jest wywoływana po odpowiedzi użytkownika.
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 jest inicjalizowany w onCreate Activity przez registerForActivityResult, który zapewnia dostęp do ActivityResultRegistry. Po inicjalizacji handler jest gotowy do obsługi żądań przez cały cykl życia Activity. Ważne jest, aby wywołać initialize przed pierwszym żądaniem, w przeciwnym razie launcher nie zostanie zarejestrowany.
Integracja Permission Handler z ViewModel to najbardziej zaawansowane podejście. ViewModel zarządza stanem żądań, a Handler wykonuje tylko wywołania platformowe. ViewModel zawiera StateFlow<PermissionUiState>, gdzie UiState opisuje, które uprawnienie jest żądane i jaki wynik uzyskano. Activity subskrybuje ten StateFlow i deleguje żądanie do 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()
}
W tym modelu Activity sprawdza isPermissionGranted przez Handler przy starcie, a ViewModel zarządza tylko stanem. Jeśli uprawnienie nie zostało przyznane — Activity subskrybuje uiState, wywołuje requestCamera z Handler i przekazuje wynik z powrotem do ViewModel przez onPermissionResult. Rozdzielenie kodu platformy i logiki biznesowej pozwala testować ViewModel bez zależności Androida.
Testy jednostkowe Permission Handler są możliwe dzięki interfejsowi PermissionHandler. W testach tworzony jest FakePermissionHandler, który imituje różne scenariusze: uprawnienie przyznane, odrzucone, Never Ask Again. Każdy scenariusz jest testowany niezależnie. Jest to szczególnie ważne dla testowania logiki UI, która musi poprawnie reagować na wszystkie trzy wyniki.
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
}
}
Implementacja Fake pozwala testować ViewModel bez emulatora. Wystarczy ustawić cameraResult na odpowiednią wartość i sprawdzić, czy ViewModel poprawnie aktualizuje UiState. Testy integracyjne sprawdzają rzeczywisty PermissionHandler z ActivityScenario, ale zwykle takich testów jest 2-3 na całą aplikację — pozostałe scenariusze są pokryte testami jednostkowymi z fake'ami.
Typowe błędy podczas pracy z Permission Handler obejmują: brak sprawdzenia checkSelfPermission przed każdym wywołaniem API, ignorowanie shouldShowRequestPermissionRationale, ponowne wywołanie requestPermissions przy Never Ask Again oraz przechowywanie launcherów bez uwzględnienia cyklu życia Activity. Przyjrzyjmy się każdemu problemowi i rozwiązaniu.
Najczęstszym błędem jest wywołanie API bez sprawdzenia statusu uprawnienia. Deweloperzy zakładają, że jeśli uprawnienie zostało raz przyznane, pozostanie na zawsze. Jednak użytkownik może je cofnąć w ustawieniach w dowolnym momencie. Permission Handler powinien zawsze wywoływać isPermissionGranted przed wykonaniem wrażliwej operacji. Drugim popularnym błędem jest ignorowanie shouldShowRequestPermissionRationale i ponowne żądanie, które prowadzi do natychmiastowej odmowy bez dialogu przy Never Ask Again.
Najlepsze praktyki obejmują: utworzenie jednej instancji Handler na cały cykl życia Activity, używanie SharedFlow do przekazywania wyników do ViewModel, logowanie wszystkich żądań i odmów dla analityki, a także wyświetlanie niestandardowego dialogu rationale przed systemowym przy pierwszej odmowie. Przestrzeganie tych zasad gwarantuje stabilną pracę z uprawnieniami na wszystkich wersjach Androida.
Często zadawane pytania
Permission Handler — to komponent do scentralizowanego zarządzania uprawnieniami runtime, hermetyzujący checkSelfPermission, requestPermissions i shouldShowRequestPermissionRationale. Upraszcza utrzymanie kodu i poprawia testowanie.
Zalecany jest ActivityResultContracts.RequestPermission z biblioteki androidx.activity. Zastępuje on przestarzały onRequestPermissionsResult i zapewnia czyste callback API z wynikiem Boolean.
Dla jednego uprawnienia Handler nie jest obowiązkowy — można użyć bezpośredniego wywołania RequestPermission lauchera w Activity. Handler staje się niezbędny przy 3 i więcej uprawnieniach, aby uniknąć powielania kodu.
Utwórz interfejs PermissionHandler i jego fake-implementację do testów jednostkowych. Fake zwraca predefiniowane wyniki bez wywołań systemowych. Pozwala to testować ViewModel i logikę UI bez emulatora.
Po odmowie sprawdź shouldShowRequestPermissionRationale. Jeśli metoda zwróciła false — aktywowano tryb Never Ask Again. Handler powinien zwrócić PermissionResult.DENIED(false), a UI — wyświetlić przycisk przejścia do Ustawień.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również