SkillAgentSearch skills...

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

README

<div align="center"> <img src="assets/logo.svg" height="108" alt="Grant" />

Maven Central Kotlin CI License

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 GRANTED by 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.
View on GitHub
GitHub Stars79
CategoryDevelopment
Updated9h ago
Forks3

Languages

Kotlin

Security Score

100/100

Audited on Apr 8, 2026

No findings