Grant
Kotlin Multiplatform permission library for Android & iOS. No Fragment/Activity needed, ViewModel-first, Compose Multiplatform ready. Fixes Android dead clicks & iOS deadlocks.
Install / Use
/learn @brewkits/GrantREADME
Type-safe permission handling for Kotlin Multiplatform — the parts Android and iOS both make hard.
</div>Grant simplifies permission handling across Android and iOS with a clean, type-safe API. No Fragment/Activity required, no binding boilerplate, and built-in support for service checking (GPS, Bluetooth, etc.).
Key Features:
- Clean, enum-based API that works anywhere (ViewModels, repositories, Composables)
- iOS Info.plist validation to prevent crashes
- Android process death recovery without timeout
- Extensible design supporting custom permissions via RawPermission
- Built-in service status checking (Location, Bluetooth)
- Atomic Group Requests for optimized system dialog UX
- Partial Granted detection (Android 14+ "Select Photos")
🚀 Quick Start
// 1️⃣ In your ViewModel
class CameraViewModel(grantManager: GrantManager) : ViewModel() {
val cameraGrant = GrantHandler(
grantManager = grantManager,
grant = AppGrant.CAMERA,
scope = viewModelScope
)
fun openCamera() {
cameraGrant.request {
// ✅ This runs ONLY when permission is granted
startCameraCapture()
}
}
}
// 2️⃣ In your Compose UI
@Composable
fun CameraScreen(viewModel: CameraViewModel) {
GrantDialog(handler = viewModel.cameraGrant) // Handles all dialogs automatically
Button(onClick = { viewModel.openCamera() }) {
Text("Take Photo")
}
}
Simple and straightforward - no Fragment, no BindEffect, no manual configuration.
Why Grant?
The Traditional Approach (moko-permissions / accompanist)
Traditional KMP permission libraries require lifecycle binding and boilerplate:
// ❌ TRADITIONAL: Fragment/Activity required + Boilerplate
class MyFragment : Fragment() {
private val permissionHelper = PermissionHelper(this) // Needs Fragment!
fun requestCamera() {
permissionHelper.bindToLifecycle() // BindEffect boilerplate
permissionHelper.request(Permission.CAMERA) {
// Complex state management
}
}
}
The Grant Way
// ✅ GRANT WAY: Works anywhere, zero boilerplate
@Composable
fun CameraScreen() {
val grantManager = remember { GrantFactory.create(context) }
Button(onClick = {
when (grantManager.request(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> openSettings()
}
}) { Text("Take Photo") }
}
Simple, clean, and works anywhere — ViewModels, repositories, or Composables.
Comparison
| Feature | Grant | moko-permissions | accompanist-permissions | |---------|-------|-----------------|------------------------| | No Fragment/Activity | ✅ | ❌ (needs BindEffect) | ❌ (needs Activity) | | ViewModel-first | ✅ | Partial | ❌ | | Info.plist Validation | ✅ | ❌ | ❌ | | Process Death Recovery | ✅ | ❌ | Manual | | Custom Permissions | ✅ RawPermission | Limited | Limited | | Service Checking | ✅ Built-in | ❌ Separate | ❌ Separate | | Android 14 Partial Gallery | ✅ | Varies | ✅ | | Enum-Based Status | ✅ | ✅ | ❌ Multiple APIs | | iOS Deadlock Fix | ✅ | ❌ | N/A | | Android Dead Click Fix | ✅ | ❌ | ❌ | | Cross-Platform | Android + iOS | Android + iOS | Android only |
Platform Support
| Platform | Version | Notes |
|----------|---------|-------|
| Android | API 24+ | Full support for Android 12, 13, 14 (Partial Gallery Access) |
| iOS | 13.0+ | Crash-guard & Main thread safety built-in |
| Compose | 1.7.1+ | Separate grant-compose module with GrantDialog |
💡 Note: See iOS Info.plist Setup and iOS Setup Guide for detailed configuration.
Using with Web/Desktop Targets
Grant currently provides full implementations for Android and iOS only. If your Kotlin Multiplatform project targets additional platforms like js (browser) or jvm (desktop), you cannot add Grant directly to commonMain as Gradle requires artifacts for all declared targets.
Recommended Approach: Use an Intermediate Source Set
The best practice is to create a mobile-specific source set (e.g., mobileMain) that sits between commonMain and your Android/iOS source sets:
// shared/build.gradle.kts
kotlin {
// Define your targets
androidTarget()
iosArm64()
iosSimulatorArm64()
js() // Web target
jvm() // Desktop target
sourceSets {
// Create mobile-specific source set
val mobileMain by creating {
dependsOn(commonMain.get())
}
// Android and iOS depend on mobileMain
androidMain.get().dependsOn(mobileMain)
iosMain.get().dependsOn(mobileMain)
// Add Grant dependencies to mobileMain
mobileMain.dependencies {
implementation("dev.brewkits:grant-core:1.2.0")
implementation("dev.brewkits:grant-compose:1.2.0") // Optional
}
// Your JS/JVM code remains in commonMain without Grant
commonMain.dependencies {
// Other shared dependencies
}
}
}
Benefits:
- ✅ Keeps your mobile permission logic separate from web/desktop code
- ✅ No compilation errors from missing JS/JVM artifacts
- ✅ Clean architecture following KMP best practices
- ✅ No risk of false-positive permission grants on unsupported platforms
Future Roadmap
We're exploring native implementations for JS (Browser Permissions API) and JVM (macOS TCC) in future releases, rather than simple no-op stubs. This would provide real value for web and desktop applications.
Why not no-op stubs? Modern browsers and desktop platforms (macOS, Windows) have their own permission systems. A no-op implementation that returns
GRANTEDby default would be misleading and cause runtime failures when your app tries to access protected resources (camera, microphone, location). Any future implementation will properly integrate with platform-specific permission APIs.
For updates, follow Issue #19.
Demo
Run the demo app to see all 17 permissions in action:
./gradlew :demo:installDebug # Android
# Or open iosApp in Xcode for iOS
Key Features
Clean API Design
- No Fragment/Activity required - Works in ViewModels, repositories, or Composables
- No lifecycle binding - No BindEffect or manual lifecycle management
- Enum-based status - Type-safe, predictable flow control
- Coroutine-first - Async by default with suspend functions
Platform-Specific Handling
- Android 12+ Dead Click Fix - Handles Android 12+ dead clicks automatically
- Android 14 Partial Gallery Access - Supports "Select Photos" mode
- iOS Deadlock Prevention - Fixes Camera/Microphone deadlock on first request
- Granular Gallery Permissions - Separate handling for images vs videos
Production Safety
-
iOS Info.plist Validation - Validates keys before calling native APIs
- Prevents SIGABRT crashes from missing configuration
- Returns DENIED_ALWAYS with error message instead of crashing
-
Android Process Death Recovery - Handles process death gracefully
- Instant recovery without timeout
- Automatic orphan cleanup
- Dialog state restoration via savedInstanceState
Architecture
- Thread-safe - Proper mutex handling for concurrent requests
- Memory efficient - Application context only, no Activity retention
- Well-tested - 103 unit tests covering core functionality
- Extensible - Sealed interface design supports custom permissions
Built-in Service Checking
Permissions don't guarantee services are enabled. Grant includes service status checking:
val serviceManager = ServiceFactory.create(context)
// ✅ Check if Location service is enabled (not just permission!)
when {
!serviceManager.isLocationEnabled() -> {
// GPS is OFF - guide user to enable it
serviceManager.openLocationSettings()
}
grantManager.checkStatus(AppGrant.LOCATION) == GrantStatus.GRANTED -> {
// Both permission AND service are ready!
startLocationTracking()
}
}
// ✅ Check Bluetooth service status
if (!serviceManager.isBluetoothEnabled()) {
serviceManager.openBluetoothSettings()
}
Supported Services:
- Location - GPS/Network location services
- Bluetooth - Bluetooth adapter status
- Background Location - Platform-specific checks
This helps you detect when users grant permission but forget to enable the required service.
Cross-Platform Coverage
- Android: API 24+
- iOS: iOS 13.0+
- 17 Permission Types: Camera, Microphone, Gallery (Images/Videos/Both), Storage, Location, Location Always, Notifications, Schedule Exact Alarm, Bluetooth, Bluetooth Advertise, Contacts, Read Contacts, Motion, Calendar, Read Calendar
📦 Installation
Gradle (Kotlin DSL)
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:1.2.0")
implementation("dev.brewkits:grant-compose:1.
