Permission Handlerは、ランタイム権限の確認、リクエスト、結果の処理を担当するAndroidアプリケーションコンポーネントです。Android Developer Guide, 2024によると、権限ハンドラーはcheckSelfPermission、requestPermissions、shouldShowRequestPermissionRationaleのロジックを単一のクラスまたはViewModelに集中化します。これによりコードの保守が簡素化され、テストが改善されます。
主なポイント
Permission Handlerは、Androidランタイム権限を集中管理するためのアーキテクチャパターンです。アプリケーションコード全体に散在するContextCompat.checkSelfPermissionやActivityCompat.requestPermissionsの個別の呼び出しの代わりに、すべてのリクエストと結果処理のロジックが単一のクラスに集中されます。これにより重複が減り、保守が簡素化され、コードの予測可能性が向上します。
Permission Handlerの必要性は、Android 6.0でのランタイム権限の導入とともに生じました。それ以前は、すべての権限はインストール時に要求され、アプリケーションコードはチェックなしで任意のAPIを使用できました。ランタイムモデルに移行した後、危険な権限を使用するたびに、checkSelfPermission、requestPermissions、onRequestPermissionsResultの3段階のチェックが必要になります。このロジックをActivityやFragmentに分散させると、インラインの重複とエラーが発生します。Google I/O 2019によると、権限処理を集中化すると、Permission Denial関連のバグの数が平均60パーセント減少します。
優れたPermission Handlerは、呼び出し元のコードにクリーンなインターフェースを提供します。ActivityやFragmentはリクエストの詳細を知る必要はありません。requestCamera(callback)のようなメソッドを呼び出すだけで、ハンドラー自体がステータスチェック、理由の表示、システムダイアログの呼び出し、コールバックへの結果の受け渡しを管理します。これにより単一責任の原則が実装され、ビジネスロジックがプラットフォームの権限コードから分離されます。
アプリケーションが3つ以上の危険な権限を使用する場合、Permission Handlerが必要になります。1つの権限(QRコードスキャナーのカメラなど)のみを使用するシンプルなアプリケーションでは、直接呼び出しで十分な場合があります。しかし、カメラ、位置情報、通知、ストレージを使用する一般的なモバイルアプリケーションでは、集中化されたハンドラーが保守のために不可欠です。
典型的なPermission Handlerは、インターフェースコントラクト、ActivityResultLauncherを使用した実装、ViewModel層の3層で構成されます。インターフェースは各権限のリクエストメソッドを定義します — requestCamera、requestLocation、requestStorage。実装はこれらのメソッドを対応するActivityResultContracts.RequestPermissionコントラクトにバインドします。
アーキテクチャの主要コンポーネント:
このアーキテクチャにより、テストでの実装の交換が容易になります。実際のActivityResultLauncherの代わりに、システムとの対話なしで事前定義された結果を返すモックが使用されます。これは、権限ダイアログのためにActivityを起動することが不可能なUIロジックの単体テストに不可欠です。
Permission HandlerはActivityとFragmentのライフサイクルを考慮する必要があります。ランチャーはActivityResultRegistryに登録され、画面の回転やActivityの再作成時に自動的に状態を保存および復元します。ハンドラーはActivityやFragmentへの直接参照を保存すべきではありません。代わりに、WeakReferenceを使用するか、コンストラクタを介してregistryを渡します。これにより、設定変更時のメモリリークやクラッシュを防ぎます。
Permission Handlerの基本的な実装は、ActivityResultContracts.RequestPermissionに基づいて構築されます。ハンドラーはComponentActivityまたはFragmentからActivityResultRegistryを受け取り、各権限のランチャーを登録します。各ランチャーは、ユーザーが応答した後に呼び出されるコールバックラムダを受け入れます。
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
}
}
ハンドラーは、ActivityResultRegistryへのアクセスを提供するregisterForActivityResultを介して、ActivityのonCreateで初期化されます。初期化後、ハンドラーは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は起動時にHandlerを介してisPermissionGrantedをチェックし、ViewModelは状態のみを管理します。権限が付与されていない場合、ActivityはuiStateを購読し、HandlerのrequestCameraを呼び出し、結果をonPermissionResultを介してViewModelに返します。プラットフォームコードをビジネスロジックから分離することで、Android依存関係なしでViewModelをテストできます。
PermissionHandlerインターフェースのおかげで、Permission Handlerの単体テストが可能です。テストでは、権限付与、拒否、Never Ask AgainなどのさまざまなシナリオをシミュレートするFakePermissionHandlerが作成されます。各シナリオは独立してテストされます。これは、3つの結果すべてに正しく反応する必要がある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
}
}
フェイク実装により、エミュレーターなしでViewModelをテストできます。cameraResultを目的の値に設定し、ViewModelがUiStateを正しく更新することを確認するだけです。統合テストはActivityScenarioを使用して実際のPermissionHandlerをチェックしますが、通常はアプリケーション全体で2〜3個のテストのみで、残りのシナリオはフェイクを使用した単体テストでカバーされます。
Permission Handlerを使用する際の典型的な間違いには、各API呼び出しの前にcheckSelfPermissionをチェックしない、shouldShowRequestPermissionRationaleを無視する、Never Ask Again後にrequestPermissionsを再度呼び出す、Activityのライフサイクルを考慮せずにランチャーを保存するなどがあります。各問題とその解決策を見てみましょう。
最も一般的な間違いは、権限ステータスをチェックせずにAPIを呼び出すことです。開発者は、一度権限が付与されれば永久に有効であると想定します。しかし、ユーザーは設定からいつでも取り消すことができます。Permission Handlerは、機密性の高い操作を実行する前に常にisPermissionGrantedを呼び出す必要があります。2番目によくある間違いは、shouldShowRequestPermissionRationaleを無視してリクエストを繰り返すことであり、Never Ask Againモードではダイアログなしで即座に拒否されます。
ベストプラクティスには、Activityのライフサイクル全体で単一のHandlerインスタンスを作成すること、結果をViewModelに渡すためにSharedFlowを使用すること、分析のためにすべてのリクエストと拒否をログに記録すること、最初の拒否時にシステムダイアログの前にカスタム理由ダイアログを表示することが含まれます。これらのルールに従うことで、すべてのAndroidバージョンで安定した権限処理が保証されます。
よくある質問
Permission Handlerは、ランタイム権限を集中管理するためのコンポーネントで、checkSelfPermission、requestPermissions、shouldShowRequestPermissionRationaleをカプセル化します。コードの保守を簡素化し、テストを改善します。
androidx.activityライブラリのActivityResultContracts.RequestPermissionを使用することをお勧めします。これは非推奨のonRequestPermissionsResultを置き換え、Boolean結果によるクリーンなコールバックAPIを提供します。
1つの権限の場合、Handlerは必須ではありません。ActivityでRequestPermissionランチャーを直接呼び出すことができます。コードの重複を避けるために、3つ以上の権限がある場合にHandlerが必要になります。
単体テスト用にPermissionHandlerインターフェースとそのフェイク実装を作成します。フェイクはシステムコールなしで事前定義された結果を返します。これにより、エミュレーターなしでViewModelとUIロジックをテストできます。
拒否後、shouldShowRequestPermissionRationaleを確認します。メソッドがfalseを返す場合、Never Ask Againモードがアクティブです。HandlerはPermissionResult.DENIED(false)を返し、UIは設定に移動するボタンを表示する必要があります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。