Skip to content
/ Grant Public

The Headless Permission Manager for Kotlin Multiplatform. Clean Architecture & ViewModel-first. Features: Fixed Android 'Dead Clicks', Unified Service Checking (GPS/Bluetooth/NFC), Process Death recovery, and First-class Compose Multiplatform support.

License

Notifications You must be signed in to change notification settings

brewkits/Grant

Grant 🎯

Kotlin Compose Maven Central License

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)

⚡ Quick Start (30 seconds)

// 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 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.


🎬 See It In Action

📸 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

Why Grant? 🎯

The Traditional Approach

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
        }
    }
}

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") }
}

That's it. No Fragment. No binding. No dead clicks.


🚀 Key Features

✨ Zero Boilerplate - Revolutionary Simplicity

  • 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

🛡️ Smart Platform Handling - Fixes Industry-Wide Bugs

  • 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-

🏆 Production-Grade Features

  • 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

🏗️ Production-Ready Architecture - Enterprise Grade

  • 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

🛠️ Built-in Service Checking - The Missing Piece

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

📱 Cross-Platform Coverage

  • 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

📦 Installation

Gradle (Kotlin DSL)

// 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
        }
    }
}

🎯 Quick Start

Basic Usage

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.


📋 Supported Permissions

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

🔧 Extensibility: Custom Permissions

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.

Why This Matters

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

Custom Permission Examples

Android 15 New Permission

// 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 New Permission

// 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)

Cross-Platform Custom Permission

// 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
)

Android 14+ Partial Photo Picker

// 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"
)

How It Works

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?
) : GrantPermission

Benefits:

  • AppGrant for common permissions (compile-time safety)
  • RawPermission for new/custom permissions (runtime flexibility)
  • Both work with GrantManager.request(), GrantHandler, and GrantDialog
  • No library update needed for OS updates

Important Notes

  1. Platform Compatibility: You're responsible for checking API levels

    if (Build.VERSION.SDK_INT >= 34) {
        grantManager.request(android14Permission)
    }
  2. Manifest Declaration: Remember to add permissions to AndroidManifest.xml

    <uses-permission android:name="android.permission.YOUR_CUSTOM_PERMISSION" />
  3. iOS Info.plist: Add usage description keys

    <key>NSYourCustomUsageDescription</key>
    <string>We need this permission because...</string>

Real-World Use Cases

  • 🆕 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

📊 Library Comparison

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

Grant's Unique Features

Key Differentiators:

  1. Info.plist validation - No production crashes from missing config
  2. Process death recovery - Zero timeout, no memory leaks
  3. Service checking - GPS/Bluetooth status built-in
  4. RawPermission extensibility - Custom permissions without library updates
  5. 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

📚 Documentation

Getting Started

Core Concepts

Platform Guides

Advanced Topics

Production Checklist

  • 🔒 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

🛠️ Configuration

Enable Logging (Development Only)

import dev.brewkits.grant.utils.GrantLogger

// Enable logging during development
GrantLogger.isEnabled = true

// ⚠️ IMPORTANT: Disable for production release
GrantLogger.isEnabled = false

Benefits 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

Custom Log Handler

// 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")
    }
}

🤝 Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.


📄 License

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.

⭐ Star Us on GitHub!

If Grant saves you time, please give us a star!

It helps other developers discover this project.

⬆️ Back to Top


Made with ❤️ by Nguyễn Tuấn Việt at Brewkits

Support: datacenter111@gmail.comCommunity: GitHub Issues

⭐ Star on GitHub📦 Maven Central

About

The Headless Permission Manager for Kotlin Multiplatform. Clean Architecture & ViewModel-first. Features: Fixed Android 'Dead Clicks', Unified Service Checking (GPS/Bluetooth/NFC), Process Death recovery, and First-class Compose Multiplatform support.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published