
Production-ready permission manager eliminating lifecycle boilerplate and fragments; automatic rationale/settings dialogs, service checks for GPS/Bluetooth, dead-click and gallery permission edge-case fixes, thread-safe API.
Production-ready, type-safe permission handling for Kotlin Multiplatform — handling the complex edge cases of Android and iOS flows.
Grant is not just another permission library. It is a production-hardened engine designed to handle complex edge cases that lead to crashes and hangs in other solutions. Built for professionals who demand absolute reliability.
NSUsageDescription requirements.Info.plist keys before requesting, preventing the dreaded SIGABRT production crashes.SavedStateHandle).ReentrantMutex prevents deadlocks in complex nested permission flows.BasicAlertDialog in the Compose module.IosPermissionHandlerRegistry or use RawPermission.class CameraViewModel(private val grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope
)
// Option A: Suspend function (Modern Coroutines)
suspend fun startCapture() {
val status = cameraGrant.requestSuspend()
if (status == GrantStatus.GRANTED) {
cameraEngine.start()
}
}
// Option B: Flow-based (Reactive)
val captureFlow = cameraGrant.requestFlow()
.filter { it == GrantStatus.GRANTED }
.onEach { cameraEngine.start() }
}val scanFlow = grantFlow {
// 1. First, we need Bluetooth
val btStatus = bluetoothHandler.requestSuspend()
// 2. If granted, we need Location (for scanning on some Android versions)
if (btStatus == GrantStatus.GRANTED) {
locationHandler.requestSuspend()
}
}@Composable
fun CameraScreen(viewModel: CameraViewModel) {
// Automatically uses Material 3 BasicAlertDialog for a modern look
GrantDialog(handler = viewModel.cameraGrant)
Button(onClick = { viewModel.viewModelScope.launch { viewModel.startCapture() } }) {
Text("Start Camera")
}
}Permission is only half the battle. In production, you also need to check if the hardware service (GPS, Bluetooth) is actually enabled.
// Use GrantAndServiceChecker to combine both worlds
class LocationViewModel(
private val checker: GrantAndServiceChecker,
private val grantManager: GrantManager
) : ViewModel() {
fun startTracking() {
viewModelScope.launch {
when (val status = checker.checkLocationReady()) {
LocationReadyStatus.Ready -> sensor.start()
LocationReadyStatus.ServiceDisabled -> _uiState.showEnableGPS()
LocationReadyStatus.GrantDenied -> requestPermission()
LocationReadyStatus.BothRequired -> _uiState.showTotalFailure()
}
}
}
}Most KMP permission libraries are simple wrappers around native APIs. Grant is an Architectural Solution.
| Feature | Grant | moko-permissions | accompanist-permissions |
|---|---|---|---|
| No Lifecycle Binding | ✅ | ❌ (needs BindEffect) | ❌ (needs Activity) |
| ViewModel Support | Full | Partial | ❌ |
| iOS Crash Prevention | ✅ | ❌ | ❌ |
| iOS Framework Isolation | ✅ | ❌ | N/A |
| Android Deadlock Fix | ✅ | ❌ | ❌ |
| Process Death Recovery | Native | ❌ | Manual |
| Service Checks (GPS/BT/Health) | ✅ | ❌ | ❌ |
| Android 14 Partial Access | ✅ | Partial | ✅ |
| Custom Permissions | ✅ | Limited | Limited |
| Permission | Android | iOS | Notes |
|---|---|---|---|
| Camera | ✅ | ✅ | iOS main-thread safe + deadlock fix |
| Microphone | ✅ | ✅ | Shares AVFoundation handler with Camera |
| Gallery (full) | ✅ | ✅ | Android 14+ partial access (PARTIAL_GRANTED) |
| Gallery (images only) | ✅ | ✅ | AppGrant.GALLERY_IMAGES_ONLY |
| Gallery (video only) | ✅ | ✅ | AppGrant.GALLERY_VIDEO_ONLY |
| Storage (legacy) | ✅ | ✅ | Pre-API 33 fallback |
| Location (when in use) | ✅ | ✅ | Intelligent GPS service check included |
| Location (always) | ✅ | ✅ | Android 2-step background flow handled |
| Notifications | ✅ | ✅ | Android 13+ and legacy flows |
| Bluetooth | ✅ | ✅ | Service status check + Scan/Connect |
| Bluetooth Advertise | ✅ | ✅ | AppGrant.BLUETOOTH_ADVERTISE |
| Contacts (full) | ✅ | ✅ | Read + Write access |
| Contacts (read-only) | ✅ | ✅ | AppGrant.READ_CONTACTS |
| Calendar (full) | ✅ | ✅ | iOS 17+ FullAccess / WriteOnly mapped correctly |
| Calendar (read-only) | ✅ | ✅ | AppGrant.READ_CALENDAR |
| Motion / Activity | ✅ | ✅ | Simulator-aware (safe mock on Simulator) |
| Schedule Exact Alarm | ✅ | ✅ | Android 12+ SCHEDULE_EXACT_ALARM
|
| Service | Android | iOS |
|---|---|---|
| GPS / Location | ✅ | ✅ |
| Bluetooth | ✅ | ✅ |
| Wi-Fi | ✅ | ✅ |
| NFC | ✅ | — |
| Camera hardware | ✅ | ✅ |
| Health Connect / HealthKit | ✅ | ✅ |
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:2.0.0")
implementation("dev.brewkits:grant-compose:2.0.0") // Optional: Compose dialogs
implementation("dev.brewkits:grant-core-koin:2.0.0") // Optional: Koin DI support
// Optional: add only the permission modules you actually use on iOS.
// Omitting a module means its iOS framework is never linked — no phantom
// NSUsageDescription keys, no App Store rejections.
implementation("dev.brewkits:grant-contacts:2.0.0") // Optional: Contacts (iOS CNContactStore)
implementation("dev.brewkits:grant-calendar:2.0.0") // Optional: Calendar (iOS EventKit)
implementation("dev.brewkits:grant-motion:2.0.0") // Optional: Motion (iOS CoreMotion)
}
}
}iOS-only step: call initialize() once at app startup for each optional module you added:
// iOS — AppDelegate / @main entry point
GrantContacts.shared.initialize() // if you added grant-contacts
GrantCalendar.shared.initialize() // if you added grant-calendar
GrantMotion.shared.initialize() // if you added grant-motion[!IMPORTANT] For projects targeting Web (JS) or Desktop (JVM), use an intermediate
mobileMainsource set to avoid linking iOS/Android dependencies on unsupported platforms. Read the Guide.
[!NOTE] Migrating from v1.x? Contacts, Calendar, and Motion permissions are now opt-in modules. Add the corresponding artifact and call
initialize()on iOS. Android behavior is unchanged — no code changes required on Android. See the Migration Guide.
| Guide | Description |
|---|---|
| Architecture | How concurrency, state machines, and the mutex flow work |
| iOS Setup | Critical Info.plist configuration — read before shipping |
| Migration Guide | Upgrading from v1.x to v2.0.0 |
| Service Checking | Combining permission + hardware service checks |
| Manual Injection | Using Grant without any DI framework |
| Android Reliability | How we fix "Dead Clicks" on Android |
| Best Practices | Patterns for production apps |
We are on a mission to make permissions a "solved problem" for KMP. Join us!
./gradlew :grant-core:allTests to ensure stability.Grant is licensed under the Apache License 2.0. See LICENSE for details.
Production-ready, type-safe permission handling for Kotlin Multiplatform — handling the complex edge cases of Android and iOS flows.
Grant is not just another permission library. It is a production-hardened engine designed to handle complex edge cases that lead to crashes and hangs in other solutions. Built for professionals who demand absolute reliability.
NSUsageDescription requirements.Info.plist keys before requesting, preventing the dreaded SIGABRT production crashes.SavedStateHandle).ReentrantMutex prevents deadlocks in complex nested permission flows.BasicAlertDialog in the Compose module.IosPermissionHandlerRegistry or use RawPermission.class CameraViewModel(private val grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope
)
// Option A: Suspend function (Modern Coroutines)
suspend fun startCapture() {
val status = cameraGrant.requestSuspend()
if (status == GrantStatus.GRANTED) {
cameraEngine.start()
}
}
// Option B: Flow-based (Reactive)
val captureFlow = cameraGrant.requestFlow()
.filter { it == GrantStatus.GRANTED }
.onEach { cameraEngine.start() }
}val scanFlow = grantFlow {
// 1. First, we need Bluetooth
val btStatus = bluetoothHandler.requestSuspend()
// 2. If granted, we need Location (for scanning on some Android versions)
if (btStatus == GrantStatus.GRANTED) {
locationHandler.requestSuspend()
}
}@Composable
fun CameraScreen(viewModel: CameraViewModel) {
// Automatically uses Material 3 BasicAlertDialog for a modern look
GrantDialog(handler = viewModel.cameraGrant)
Button(onClick = { viewModel.viewModelScope.launch { viewModel.startCapture() } }) {
Text("Start Camera")
}
}Permission is only half the battle. In production, you also need to check if the hardware service (GPS, Bluetooth) is actually enabled.
// Use GrantAndServiceChecker to combine both worlds
class LocationViewModel(
private val checker: GrantAndServiceChecker,
private val grantManager: GrantManager
) : ViewModel() {
fun startTracking() {
viewModelScope.launch {
when (val status = checker.checkLocationReady()) {
LocationReadyStatus.Ready -> sensor.start()
LocationReadyStatus.ServiceDisabled -> _uiState.showEnableGPS()
LocationReadyStatus.GrantDenied -> requestPermission()
LocationReadyStatus.BothRequired -> _uiState.showTotalFailure()
}
}
}
}Most KMP permission libraries are simple wrappers around native APIs. Grant is an Architectural Solution.
| Feature | Grant | moko-permissions | accompanist-permissions |
|---|---|---|---|
| No Lifecycle Binding | ✅ | ❌ (needs BindEffect) | ❌ (needs Activity) |
| ViewModel Support | Full | Partial | ❌ |
| iOS Crash Prevention | ✅ | ❌ | ❌ |
| iOS Framework Isolation | ✅ | ❌ | N/A |
| Android Deadlock Fix | ✅ | ❌ | ❌ |
| Process Death Recovery | Native | ❌ | Manual |
| Service Checks (GPS/BT/Health) | ✅ | ❌ | ❌ |
| Android 14 Partial Access | ✅ | Partial | ✅ |
| Custom Permissions | ✅ | Limited | Limited |
| Permission | Android | iOS | Notes |
|---|---|---|---|
| Camera | ✅ | ✅ | iOS main-thread safe + deadlock fix |
| Microphone | ✅ | ✅ | Shares AVFoundation handler with Camera |
| Gallery (full) | ✅ | ✅ | Android 14+ partial access (PARTIAL_GRANTED) |
| Gallery (images only) | ✅ | ✅ | AppGrant.GALLERY_IMAGES_ONLY |
| Gallery (video only) | ✅ | ✅ | AppGrant.GALLERY_VIDEO_ONLY |
| Storage (legacy) | ✅ | ✅ | Pre-API 33 fallback |
| Location (when in use) | ✅ | ✅ | Intelligent GPS service check included |
| Location (always) | ✅ | ✅ | Android 2-step background flow handled |
| Notifications | ✅ | ✅ | Android 13+ and legacy flows |
| Bluetooth | ✅ | ✅ | Service status check + Scan/Connect |
| Bluetooth Advertise | ✅ | ✅ | AppGrant.BLUETOOTH_ADVERTISE |
| Contacts (full) | ✅ | ✅ | Read + Write access |
| Contacts (read-only) | ✅ | ✅ | AppGrant.READ_CONTACTS |
| Calendar (full) | ✅ | ✅ | iOS 17+ FullAccess / WriteOnly mapped correctly |
| Calendar (read-only) | ✅ | ✅ | AppGrant.READ_CALENDAR |
| Motion / Activity | ✅ | ✅ | Simulator-aware (safe mock on Simulator) |
| Schedule Exact Alarm | ✅ | ✅ | Android 12+ SCHEDULE_EXACT_ALARM
|
| Service | Android | iOS |
|---|---|---|
| GPS / Location | ✅ | ✅ |
| Bluetooth | ✅ | ✅ |
| Wi-Fi | ✅ | ✅ |
| NFC | ✅ | — |
| Camera hardware | ✅ | ✅ |
| Health Connect / HealthKit | ✅ | ✅ |
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:2.0.0")
implementation("dev.brewkits:grant-compose:2.0.0") // Optional: Compose dialogs
implementation("dev.brewkits:grant-core-koin:2.0.0") // Optional: Koin DI support
// Optional: add only the permission modules you actually use on iOS.
// Omitting a module means its iOS framework is never linked — no phantom
// NSUsageDescription keys, no App Store rejections.
implementation("dev.brewkits:grant-contacts:2.0.0") // Optional: Contacts (iOS CNContactStore)
implementation("dev.brewkits:grant-calendar:2.0.0") // Optional: Calendar (iOS EventKit)
implementation("dev.brewkits:grant-motion:2.0.0") // Optional: Motion (iOS CoreMotion)
}
}
}iOS-only step: call initialize() once at app startup for each optional module you added:
// iOS — AppDelegate / @main entry point
GrantContacts.shared.initialize() // if you added grant-contacts
GrantCalendar.shared.initialize() // if you added grant-calendar
GrantMotion.shared.initialize() // if you added grant-motion[!IMPORTANT] For projects targeting Web (JS) or Desktop (JVM), use an intermediate
mobileMainsource set to avoid linking iOS/Android dependencies on unsupported platforms. Read the Guide.
[!NOTE] Migrating from v1.x? Contacts, Calendar, and Motion permissions are now opt-in modules. Add the corresponding artifact and call
initialize()on iOS. Android behavior is unchanged — no code changes required on Android. See the Migration Guide.
| Guide | Description |
|---|---|
| Architecture | How concurrency, state machines, and the mutex flow work |
| iOS Setup | Critical Info.plist configuration — read before shipping |
| Migration Guide | Upgrading from v1.x to v2.0.0 |
| Service Checking | Combining permission + hardware service checks |
| Manual Injection | Using Grant without any DI framework |
| Android Reliability | How we fix "Dead Clicks" on Android |
| Best Practices | Patterns for production apps |
We are on a mission to make permissions a "solved problem" for KMP. Join us!
./gradlew :grant-core:allTests to ensure stability.Grant is licensed under the Apache License 2.0. See LICENSE for details.