GrantHandler

class GrantHandler(grantManager: GrantManager, grant: GrantPermission, scope: CoroutineScope, savedStateDelegate: SavedStateDelegate = NoOpSavedStateDelegate(), eventListener: GrantEventListener? = null)

Encapsulates ALL grant logic in a clean, reusable component.

This class follows the Composition Pattern - ViewModels compose this handler instead of implementing grant logic themselves.

Benefits:

  1. DRY: Write grant logic once, reuse everywhere

  2. Clean ViewModels: Reduce from 30+ lines to 3 lines

  3. Consistent UX: All features follow same grant flow

  4. Testable: Easy to mock and test

  5. Process Death Recovery: Optional state restoration on Android

Usage in ViewModel:

class CameraViewModel(grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA, // Or use RawPermission for custom permissions
scope = viewModelScope // CRITICAL: Use viewModelScope!
)

fun onCaptureClick() {
cameraGrant.request {
// Only runs when grant is GRANTED
openCamera()
}
}
}

Usage in UI (Compose):

GrantDialogHandler(handler = viewModel.cameraGrant)

Process Death Recovery (Android):

class CameraViewModel(
grantManager: GrantManager,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope,
savedStateDelegate = AndroidSavedStateDelegate(savedStateHandle) // Optional
)
}

Parameters

grantManager

The underlying grant manager

grant

The specific grant this handler manages (AppGrant or RawPermission)

scope

CoroutineScope for launching grant requests.

**CRITICAL - Scope Requirements:**
- **MUST** use [viewModelScope] from a ViewModel
- Survives configuration changes (screen rotation)
- Automatically cancelled when ViewModel is cleared
- Prevents memory leaks

**DO NOT USE:**
- [GlobalScope]: Never cancelled, causes memory leaks
- [lifecycleScope]: Cancelled on config changes, breaks ongoing requests
- Custom scopes with short lifecycle
- Any scope that doesn't outlive the grant flow

**Example of CORRECT usage:**
```kotlin
class MyViewModel : ViewModel() {
    val handler = GrantHandler(..., scope = viewModelScope) // ✅ Correct
}
```

**Example of WRONG usage:**
```kotlin
class MyFragment : Fragment() {
    val handler = GrantHandler(..., scope = lifecycleScope) // ❌ Wrong!
    // This breaks on screen rotation!
}
```
savedStateDelegate

Optional delegate for saving/restoring state across process death. Use AndroidSavedStateDelegate on Android for automatic restoration. Defaults to NoOpSavedStateDelegate (no persistence).

Constructors

Link copied to clipboard
constructor(grantManager: GrantManager, grant: GrantPermission, scope: CoroutineScope, savedStateDelegate: SavedStateDelegate = NoOpSavedStateDelegate(), eventListener: GrantEventListener? = null)

Properties

Link copied to clipboard
val state: StateFlow<GrantUiState>
Link copied to clipboard
val status: StateFlow<GrantStatus>

Functions

Link copied to clipboard
fun onDismiss()

Called when user dismisses any dialog (Cancel/Outside tap)

Link copied to clipboard

Called when user confirms rationale dialog and wants to proceed.

Link copied to clipboard

Call this when the app returns to foreground to check if user changed settings.

Link copied to clipboard

Called when user clicks "Open Settings" button

Link copied to clipboard

Refresh the current grant status. Call this after user returns from Settings.

Link copied to clipboard
fun request(rationaleMessage: String? = null, settingsMessage: String? = null, onGranted: (GrantStatus) -> Unit)

If a request is already in-flight, the call is logged and ignored.

Link copied to clipboard
fun requestFlow(rationaleMessage: String? = null, settingsMessage: String? = null): Flow<GrantStatus>

A Flow-based alternative to request. Returns a Flow that executes the permission request when collected.

Link copied to clipboard
suspend fun requestSuspend(rationaleMessage: String? = null, settingsMessage: String? = null): GrantStatus

A suspending alternative to request. Suspends the current coroutine until the grant flow completes (granted, denied, or dismissed).

Link copied to clipboard
fun requestWithCustomUi(rationaleMessage: String? = null, settingsMessage: String? = null, onShowRationale: (message: String, onConfirm: () -> Unit, onDismiss: () -> Unit) -> Unit, onShowSettings: (message: String, onConfirm: () -> Unit, onDismiss: () -> Unit) -> Unit, onGranted: (GrantStatus) -> Unit)

Request grant with custom UI callback handlers.