Version: 2.3.0 Last Updated: July 10, 2026
This guide helps you migrate from previous versions of Grant or other permission libraries.
- Upgrading from Grant 2.2.x to 2.3.0
- Upgrading from Grant 2.1.0 to 2.2.0
- Upgrading from Grant 1.x to 2.1.0
- Upgrading from Grant 1.3.x to 1.4.2
- From moko-permissions
- From Google Accompanist
- From Custom Implementation
- From Native Android APIs
- Common Migration Patterns
- Troubleshooting
2.3.0 is a toolchain + Android 17 release: Kotlin 2.4.0, Compose Multiplatform 1.11.1, kotlinx-coroutines 1.11.0. The public API is source-compatible with 2.2.x — for most apps the migration is bumping the version number. Three things deserve attention:
Compose Multiplatform 1.11 stopped publishing iosX64 artifacts, so grant-compose can no
longer build that target. Every other module keeps iosX64.
- Apple-silicon Macs, real devices, CI on arm64 runners: no action needed.
- If you still run the iOS simulator on an Intel Mac and use
grant-compose: stay ongrant-compose:2.2.3(it is API-compatible withgrant-core:2.3.0for the dialog surface) or drop the iosX64 target from your app.
Previously, a user who chose Approximate in the OS location dialog (grants
ACCESS_COARSE_LOCATION but not ACCESS_FINE_LOCATION) was misreported as
DENIED/DENIED_ALWAYS — even though the app held usable coarse location. 2.3.0 reports
this state as PARTIAL_GRANTED, consistent with the Android 14 partial-photos model.
What to check: anywhere you branch on AppGrant.LOCATION status, treat
PARTIAL_GRANTED as usable (coarse) access:
when (locationGrant.status.value) {
GrantStatus.GRANTED -> startPreciseTracking()
GrantStatus.PARTIAL_GRANTED -> startCoarseTracking() // NEW in 2.3.0 for approximate-only
else -> requestOrExplain()
}If you already handled PARTIAL_GRANTED for the gallery grants, the same handling applies.
If no collector is attached to GrantHandler.state (i.e. no GrantDialog / custom renderer
is composed), a DENIED / DENIED_ALWAYS flow used to suspend forever waiting for a dialog
that could never appear. 2.3.0 completes immediately with the denied status instead and
clears the unrenderable dialog state. The callback-based request() is unchanged.
What to check: if you relied on requestSuspend() never returning in that situation
(unlikely), handle the returned DENIED/DENIED_ALWAYS status.
Android 17 (API 37) introduced the ACCESS_LOCAL_NETWORK runtime permission for talking to
LAN devices (smart home, casting, printers). To adopt:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />val status = grantManager.request(AppGrant.LOCAL_NETWORK)
// Below Android 17 and on iOS this is a no-op GRANTED.On iOS there is no query/request API — the OS prompts automatically on first LAN access;
declare NSLocalNetworkUsageDescription in Info.plist.
Also fixed in 2.3.0: an Android 14+ gallery with READ_MEDIA_IMAGES + READ_MEDIA_VIDEO
granted but READ_MEDIA_VISUAL_USER_SELECTED not (ADB/MDM/auto-reset edge states) was
misreported as DENIED_ALWAYS; it now correctly reports GRANTED.
implementation("dev.brewkits:grant-core:2.3.0")
implementation("dev.brewkits:grant-compose:2.3.0") // see iosX64 note above
implementation("dev.brewkits:grant-core-koin:2.3.0")
// ...and any optional iOS modules you use, all at 2.3.0v2.2.0 (Issue #45) continues the iOS Framework Isolation work started in v2.1.0. Two more sensitive paths were moved out of grant-core into opt-in modules so apps that don't use them are never flagged by Apple's static scanner:
grant-bluetooth—CoreBluetooth.frameworkis no longer linked bygrant-core, so theNSBluetoothAlwaysUsageDescriptionrequirement disappears for apps that don't use Bluetooth.grant-location-always— therequestAlwaysAuthorization(background location) selector moved out of core.grant-corenow calls onlyrequestWhenInUseAuthorization. Apps using only foreground location are no longer asked forNSLocationAlwaysAndWhenInUseUsageDescription.
Android is completely unaffected — no code changes required on Android.
- New optional modules:
grant-bluetooth,grant-location-always. Each links its native iOS framework / selector only when added. AppGrantis unchanged:BLUETOOTH,BLUETOOTH_ADVERTISE, andLOCATION_ALWAYSare still valid enum values. On iOS they now resolve through the opt-in module's registered handler instead of a built-in one.
// shared/build.gradle.kts
commonMain.dependencies {
implementation("dev.brewkits:grant-core:2.2.0")
implementation("dev.brewkits:grant-bluetooth:2.2.0") // only if you use AppGrant.BLUETOOTH / BLUETOOTH_ADVERTISE
implementation("dev.brewkits:grant-location-always:2.2.0") // only if you use AppGrant.LOCATION_ALWAYS (background) on iOS
}// Swift
GrantBluetooth.shared.initialize()
GrantLocationAlways.shared.initialize()// iosMain — call once at app start
GrantBluetooth.initialize()
GrantLocationAlways.initialize()
⚠️ If you requestBLUETOOTH/BLUETOOTH_ADVERTISE/LOCATION_ALWAYSon iOS without adding the corresponding module and callinginitialize(), the handler is not registered:checkStatus()andrequest()log a warning and returnNOT_DETERMINED(no system dialog is shown — it does not hang or crash). On Android these permissions continue to work without any extra module.
v2.1.0 is the iOS Framework Isolation release. Contacts.framework, EventKit.framework, and CoreMotion.framework are now opt-in Gradle/Maven modules. Android is completely unaffected — no code changes required on Android.
- New optional modules:
grant-contacts,grant-calendar,grant-motion. Each module links its native iOS framework only when added. - No more forced
NSUsageDescriptionkeys: Apps that don't add an optional module are never prompted by App Store to add the corresponding usage key. IosPermissionHandlerRegistryfix:checkStatus()forRawPermissionnow correctly dispatches to custom registered handlers (previously onlyrequest()did).
// shared/build.gradle.kts
commonMain.dependencies {
implementation("dev.brewkits:grant-core:2.1.0")
}// shared/build.gradle.kts
commonMain.dependencies {
implementation("dev.brewkits:grant-core:2.1.0")
// Add only the ones your app actually uses:
implementation("dev.brewkits:grant-contacts:2.1.0") // Contacts
implementation("dev.brewkits:grant-calendar:2.1.0") // Calendar / EventKit
implementation("dev.brewkits:grant-motion:2.1.0") // CoreMotion / Step Counter
}In your iOS app entry point (e.g., AppDelegate.application(_:didFinishLaunchingWithOptions:)):
// Swift
GrantContacts.shared.initialize()
GrantCalendar.shared.initialize()
GrantMotion.shared.initialize()Or from Kotlin shared code in iosMain:
// iosMain — call once at app start
GrantContacts.initialize()
GrantCalendar.initialize()
GrantMotion.initialize()Android code, manifest permissions, and build configurations remain unchanged.
Version 1.4.2 is a critical stability release addressing high-impact bugs in Android permission flows and refining the architectural purity of the library.
- Issue #33 Fixed (Critical): Resolved a 60-second timeout on Android 11+ when requesting
LOCATION_ALWAYSby eliminating a race condition inGrantRequestActivity. - Koin Module Decoupling:
grant-coreno longer contains Koin dependencies. Usegrant-core-koinfor DI support. - Partial Upgrade Logic: Fixed
GrantHandlerto correctly trigger native OS dialogs when upgrading fromPARTIAL_GRANTEDtoGRANTED. - Android 15 Compatibility: Optimized Activity transitions and lifecycle state management for upcoming Android versions.
Update your build.gradle.kts to version 1.4.2.
If you were using grantModule or grantPlatformModule, you must now add the grant-core-koin dependency:
// shared/build.gradle.kts
commonMain.dependencies {
implementation("dev.brewkits:grant-core:1.4.2")
implementation("dev.brewkits:grant-core-koin:1.4.2") // Separate module
}Ensure you are using the latest Package.swift if integrating via SPM. v1.4.2 includes refined re-entrant locks for iOS delegates.
If you are migrating from moko-permissions, you'll find Grant's GrantHandler very similar but more focused on state flows.
| moko-permissions | Grant |
|---|---|
Permission |
AppGrant / GrantPermission |
PermissionsController |
GrantHandler |
providePermission |
request() |
// moko
controller.providePermission(Permission.CAMERA)
// Grant
handler.request()Accompanist Permissions is deprecated. Grant provides a more robust, multiplatform alternative with built-in rationale support.
| Accompanist | Grant |
|---|---|
rememberPermissionState |
GrantHandler (in ViewModel) |
permissionState.launchPermissionRequest() |
handler.request() |
permissionState.status |
handler.status (StateFlow) |
If you were manually handling ActivityResultLauncher or onRequestPermissionsResult, Grant automates this via its "Transparent Activity" pattern.
Tip: Move your logic from Activities/Fragments into ViewModels using GrantHandler.
Replace ActivityCompat.requestPermissions with GrantHandler.request. Grant handles the complexity of checking shouldShowRequestPermissionRationale and directing users to settings automatically.
Grant automatically detects when a user has permanently denied a permission and surfaces a showSettingsGuide event in the GrantUiState.
Use GrantGroupHandler to request multiple permissions at once, ensuring the UI only shows one rationale dialog for the entire group.
If you see "Koin not found" during iOS build after upgrading to 1.3.0+, ensure you have added :grant-core-koin to your commonMain dependencies and exported it if necessary.
Ensure GrantRequestActivity is registered in your AndroidManifest.xml (automatically handled by manifest merger in most cases).