The Most Comprehensive & Powerful Permission Library for Kotlin Multiplatform
🚀 No Fragment/Activity required. No BindEffect boilerplate. Smart Android 12+ handling. Built-in Service Checking.
🏆 EXCLUSIVE: Smart Config Validation + Process Death Handling - The only KMP library that won't crash from missing Info.plist keys and handles Android process death with zero timeout.
Grant is the production-ready, battle-tested permission library that eliminates boilerplate, fixes platform bugs, and handles every edge case. With zero dead clicks, memory leaks, or configuration headaches, Grant provides the cleanest API for KMP permission management.
What makes Grant unique: Through comprehensive research of the KMP permission ecosystem, Grant provides:
- Validates iOS Info.plist keys before crash (no SIGABRT in production)
- Handles Android process death with zero timeout (no 60-second hangs)
- Fixes iOS Camera/Microphone deadlock (works on first request)
- Supports custom permissions via RawPermission (extensible beyond enum)
// 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")
}
}That's it! No Fragment. No BindEffect. No configuration. Just works. ✨
| 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.
📸 Coming Soon: Live demo GIF showing the complete permission flow with GrantDialog automatically handling rationale and settings dialogs.
🎮 Try it now: Run the demo app to see all 14 permissions in action:
./gradlew :demo:installDebug # Android # Or open iosApp in Xcode for iOS
Traditional permission handling requires extensive boilerplate and lifecycle management:
// ❌ 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
}
}
}// ✅ 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") }
}That's it. No Fragment. No binding. No dead clicks.
- ✅ No Fragment/Activity required - Works in ViewModels, repositories, anywhere
- ✅ No BindEffect - No lifecycle binding ceremony
- ✅ No configuration - Works out of the box
- 🎯 Just works - One line to request, one enum to handle all states
- ✅ Android 12+ Dead Click Fix - 100% elimination of dead clicks (built-in, not a workaround)
- ✅ Android 14 Partial Gallery Access - Full support for "Select Photos" mode
- ✅ iOS Permission Deadlock Fix - Camera/Microphone work on first request (fixes critical bug #129)
- ✅ Granular Gallery Permissions - Images-only, Videos-only, or both (prevents silent denials)
- ✅ Bluetooth Error Differentiation - Retryable vs permanent errors (10s timeout)
- ✅ Notification Status Validation - Correct status even on Android 12-
-
Smart Configuration Validation (iOS) - Validates Info.plist keys before crash
- Most libraries: Instant SIGABRT crash if key missing
- Grant: Returns DENIED_ALWAYS with clear error message, app continues running
- Validates all 9 permission types before native API calls
- Production-safe handling of missing configuration
-
Robust Process Death Handling (Android) - Zero-timeout recovery system
- Common issue: 60-second hang, memory leaks, frustrated users
- Grant: Instant recovery (0ms), automatic orphan cleanup, dialog state restoration
- savedInstanceState integration for seamless UX
- Enterprise-grade handling of Android's memory management
- ✅ Enum-based Status - Clean, predictable flow (not exception-based)
- ✅ In-Memory State - Industry standard approach (90% of libraries including Google Accompanist)
- ✅ Thread-Safe - Coroutine-first design with proper mutex handling
- ✅ Memory Leak Free - Application context only, zero Activity retention
- ✅ 103+ Unit Tests - Comprehensive test coverage, all passing
- ✅ Zero Compiler Warnings - Clean, maintainable codebase
Permissions ≠ Services! Having permission doesn't mean the service is enabled. Grant is the only KMP library that handles both.
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()
}Why This Matters:
- 🎯 Users often grant permission but forget to enable GPS/Bluetooth
- 🎯 Silent failures are confusing - Grant helps you guide users properly
- 🎯 One library for both permissions AND services - no extra dependencies
Supported Services:
- Location - GPS/Network location services
- Bluetooth - Bluetooth adapter status
- Background Location - Platform-specific background location checks
- Android: API 24+ (100% coverage)
- iOS: iOS 13.0+ (100% coverage)
- 14 Permission Types: Camera, Microphone, Gallery (Images/Videos/Both), Storage, Location, Location Always, Notifications, Schedule Exact Alarm, Bluetooth, Contacts, Motion, Calendar
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.brewkits:grant-core:1.0.0")
implementation("dev.brewkits:grant-compose:1.0.0") // Optional
}
}
}import dev.brewkits.grant.*
// 1. Create manager (in ViewModel, Repository, or Composable)
val grantManager = GrantFactory.create(context)
// 2. Check current status
suspend fun checkCameraAccess() {
when (grantManager.checkStatus(AppGrant.CAMERA)) {
GrantStatus.GRANTED -> println("Camera ready!")
GrantStatus.NOT_DETERMINED -> println("Never asked")
GrantStatus.DENIED -> println("User denied, can ask again")
GrantStatus.DENIED_ALWAYS -> println("Permanently denied, go to Settings")
}
}
// 3. Request permission
suspend fun requestCamera() {
val status = grantManager.request(AppGrant.CAMERA)
when (status) {
GrantStatus.GRANTED -> openCamera()
GrantStatus.DENIED -> showRationale()
GrantStatus.DENIED_ALWAYS -> showSettingsPrompt()
GrantStatus.NOT_DETERMINED -> { /* shouldn't happen after request */ }
}
}
// 4. Check Service Status (bonus feature!)
val serviceManager = ServiceFactory.create(context)
suspend fun requestLocationWithServiceCheck() {
// First check if Location service is enabled
if (!serviceManager.isLocationEnabled()) {
// Guide user to enable GPS
serviceManager.openLocationSettings()
return
}
// Then request permission
when (grantManager.request(AppGrant.LOCATION)) {
GrantStatus.GRANTED -> startLocationTracking() // Both permission AND service ready!
else -> showError()
}
}See Quick Start Guide for complete setup.
| Permission | Android | iOS | Notes |
|---|---|---|---|
CAMERA |
✅ API 23+ | ✅ iOS 13+ | Photo/Video capture |
MICROPHONE |
✅ API 23+ | ✅ iOS 13+ | Audio recording |
GALLERY |
✅ API 23+ | ✅ iOS 13+ | Images + Videos |
GALLERY_IMAGES_ONLY |
✅ API 33+ | ✅ iOS 13+ | Images only (prevents silent denial) |
GALLERY_VIDEO_ONLY |
✅ API 33+ | ✅ iOS 13+ | Videos only (prevents silent denial) |
STORAGE |
✅ API 23+ | N/A | External storage (deprecated) |
LOCATION |
✅ API 23+ | ✅ iOS 13+ | While app in use |
LOCATION_ALWAYS |
✅ API 29+ | ✅ iOS 13+ | Background location |
NOTIFICATION |
✅ API 33+ | ✅ iOS 13+ | Push notifications |
SCHEDULE_EXACT_ALARM |
✅ API 31+ | N/A | Exact alarm scheduling |
BLUETOOTH |
✅ API 31+ | ✅ iOS 13+ | BLE scanning/connecting |
CONTACTS |
✅ API 23+ | ✅ iOS 13+ | Read contacts |
MOTION |
✅ API 29+ | ✅ iOS 13+ | Activity recognition |
CALENDAR |
✅ API 23+ | ✅ iOS 13+ | Calendar events access |
Need Android 15 permission? New OS feature not yet in the library? No problem!
Grant provides extensibility through RawPermission, allowing you to add any custom permission without waiting for library updates.
When Android 15 or iOS 18 introduces new permissions:
- Enum-based libraries: Must wait for maintainer to add permission
- Grant: Sealed interface +
RawPermission= instant extensibility
// Android 15 introduces a new permission? Use it immediately!
val predictiveBackPermission = RawPermission(
identifier = "PREDICTIVE_BACK",
androidPermissions = listOf("android.permission.PREDICTIVE_BACK"),
iosUsageKey = null // Android-only permission
)
suspend fun requestPredictiveBack() {
when (grantManager.request(predictiveBackPermission)) {
GrantStatus.GRANTED -> enablePredictiveBack()
else -> useFallback()
}
}// iOS 18 adds a new privacy key? No problem!
val healthKit = RawPermission(
identifier = "HEALTH_KIT",
androidPermissions = emptyList(), // iOS-only
iosUsageKey = "NSHealthShareUsageDescription"
)
val status = grantManager.request(healthKit)// Enterprise custom permission
val biometric = RawPermission(
identifier = "BIOMETRIC_AUTH",
androidPermissions = listOf("android.permission.USE_BIOMETRIC"),
iosUsageKey = "NSFaceIDUsageDescription"
)
// Works exactly like AppGrant.CAMERA
val handler = GrantHandler(
grantManager = grantManager,
grant = biometric, // RawPermission works seamlessly
scope = viewModelScope
)// Custom implementation for READ_MEDIA_VISUAL_USER_SELECTED (Android 14+)
val partialGallery = RawPermission(
identifier = "PARTIAL_GALLERY",
androidPermissions = listOf(
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO",
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
),
iosUsageKey = "NSPhotoLibraryUsageDescription"
)Grant uses a sealed interface architecture:
sealed interface GrantPermission {
val identifier: String
}
// Built-in permissions (type-safe, documented)
enum class AppGrant : GrantPermission {
CAMERA, LOCATION, MICROPHONE, ...
}
// Custom permissions (extensible, user-defined)
data class RawPermission(
override val identifier: String,
val androidPermissions: List<String>,
val iosUsageKey: String?
) : GrantPermissionBenefits:
AppGrantfor common permissions (compile-time safety)RawPermissionfor new/custom permissions (runtime flexibility)- Both work with
GrantManager.request(),GrantHandler, andGrantDialog - No library update needed for OS updates
-
Platform Compatibility: You're responsible for checking API levels
if (Build.VERSION.SDK_INT >= 34) { grantManager.request(android14Permission) }
-
Manifest Declaration: Remember to add permissions to
AndroidManifest.xml<uses-permission android:name="android.permission.YOUR_CUSTOM_PERMISSION" />
-
iOS Info.plist: Add usage description keys
<key>NSYourCustomUsageDescription</key> <string>We need this permission because...</string>
- 🆕 OS Updates: Android 15 new permissions (available day one)
- 🏢 Enterprise: Custom company-specific permissions
- 🧪 Testing: Experimental or proprietary permissions
- 🔧 Edge Cases: Platform-specific permissions not in
AppGrant
Why choose Grant? Here's what makes it unique:
| Feature | Grant | Other KMP Libraries | Native APIs |
|---|---|---|---|
| Zero Boilerplate | No Fragment/Activity | Requires binding | Complex setup |
| Smart Config Validation | Validates Info.plist | No validation | No validation |
| Process Death Handling | Zero timeout | Common timeout issues | Manual handling |
| iOS Deadlock Fix | Works on first request | Known issue | Known issue |
| Custom Permissions | RawPermission API | Limited to enum | Full control |
| Service Checking | Built-in GPS/BT check | Separate implementation | Separate APIs |
| Android 14 Partial Gallery | Full support | Varies by library | Full support |
| Granular Gallery Permissions | Images/Videos separate | All or nothing | Manual handling |
| Production-Safe | Comprehensive handling | Varies | Manual handling |
| Enum-Based Status | Clean flow control | Varies | Multiple APIs |
| Cross-Platform | Android + iOS | Android + iOS | Platform-specific |
Key Differentiators:
- Info.plist validation - No production crashes from missing config
- Process death recovery - Zero timeout, no memory leaks
- Service checking - GPS/Bluetooth status built-in
- RawPermission extensibility - Custom permissions without library updates
- Comprehensive bug fixes - Addresses common production issues
Production Impact:
- No crashes from missing iOS config
- No hangs from Android process death
- No dead clicks from Android 12+ issues
- Better UX with automatic dialog handling
- Quick Start Guide - Get running in 5 minutes
- Quick Start (iOS) - iOS-specific setup
- iOS Setup in Android Studio - Complete iOS setup guide
- Permission Types - All supported permissions
- Service Checking - Check GPS, Bluetooth, etc.
- Architecture - System design and patterns
- GrantStore - State management, persistence, backup rules
- Android: Dead Click Fix - Fixing Android 12+ dead clicks
- iOS: Info.plist Setup
⚠️ Critical • Simulator Limitations • Info.plist Localization
- Testing Guide - Unit testing with FakeGrantManager
- Best Practices - Production-ready patterns
- Compose Integration - Using grant-compose module
- Dependency Management - Handling version conflicts
- 🔒 iOS Info.plist: Add required keys (app crashes if missing)
- 🔧 Logging: Disable in production:
GrantLogger.isEnabled = false - 📦 Backup: Exclude GrantStore from backup if using persistent storage
- ✅ Testing: Test all permission flows on real devices
import dev.brewkits.grant.utils.GrantLogger
// Enable logging during development
GrantLogger.isEnabled = true
// ⚠️ IMPORTANT: Disable for production release
GrantLogger.isEnabled = falseBenefits of logging:
- ✅ Detects missing iOS Info.plist keys before crash
- ✅ Shows permission flow: request → rationale → settings
- ✅ Logs platform-specific behaviors (Android 12+, iOS deadlocks)
- ✅ Helps debug denied/denied_always states
// Integrate with your logging framework (Timber, Napier, etc.)
GrantLogger.logHandler = { level, tag, message ->
when (level) {
GrantLogger.LogLevel.ERROR -> Timber.e("[$tag] $message")
GrantLogger.LogLevel.WARNING -> Timber.w("[$tag] $message")
GrantLogger.LogLevel.INFO -> Timber.i("[$tag] $message")
GrantLogger.LogLevel.DEBUG -> Timber.d("[$tag] $message")
}
}We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Copyright 2026 BrewKits
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
If Grant saves you time, please give us a star!
It helps other developers discover this project.
Made with ❤️ by Nguyễn Tuấn Việt at Brewkits
Support: datacenter111@gmail.com • Community: GitHub Issues