Runtime permissions for a Compose Multiplatform app (android, iosArm64, iosSimulatorArm64): one composable call reads a permission's status, asks for it, and sends the user to settings once it is refused — on Android through the activity-result launcher, on iOS through AVFoundation, Photos, UserNotifications and Core Location.
@Composable
fun ScanButton(onScan: () -> Unit) {
val camera = rememberPermissionState(AppPermission.Camera)
when {
camera.status.isGranted -> Button(onClick = onScan) { Text("Scan") }
camera.status.canRequest -> Button(onClick = camera.request) { Text("Allow the camera") }
else -> Button(onClick = camera.openSettings) { Text("Open settings") }
}
}Statuses are re-read whenever the app returns to the foreground, so a change made in settings shows at once.
| Read | For |
|---|---|
| this file | what is here and how it is built |
permissions/api/README.md |
the contract, task by task: asking, several at once, rationale, settings, previews, tests, adding a permission |
permissions/README.md |
why each part has its shape, and what changed from the apps it came from |
skills/permissions-kit |
the same for an agent working in an app that uses the kit |
An application takes the kit by copy, not as a dependency: the code is copied into the app, renamed to the app's own package, and belongs to the app from then on. Nothing is published to a Maven repository.
If you have access to the author's knowledge repository (github.com/Thernal/knowledge), its skill-manager skill does all of it — copy, rename, the skill, and later updates:
skillctl.sh kit install permissions-kit --package com.example.app --module :core:permissions \
--alias appIt copies the code parts of kit.yml renamed, installs the permissions-kit skill and records the copy in kits.lock. kit status then shows what changed upstream and what the app edited; kit update merges the kit's changes three ways, keeping the app's edits. The install prints what the app must provide (requires).
The same by hand, from a clone of this repository.
-
Copy the paths listed under
codeinkit.ymlinto the app, under the module path the app gives them:permissions/…→core/permissions/…. Note the commit you copied (git rev-parse HEAD) — updates start from it. -
Rename in everything copied:
In the kit Becomes Where io.thernal.permissionskitthe app's package, e.g. com.example.appsources, build files; and the directories io/thernal/permissionskit:permissions:and":permissions",projects.permissions.the module path, e.g. :core:permissions:,projects.core.permissions.build files libs.plugins.permissionskit.the app's catalog alias, e.g. libs.plugins.app.build files # in the app, after copying — perl, so it runs the same on macOS and Linux grep -rlI -e io.thernal.permissionskit -e io/thernal/permissionskit -e :permissions -e plugins.permissionskit. core/permissions \ | xargs perl -pi -e 's/\Qio.thernal.permissionskit\E/com.example.app/g; s{\Qio/thernal/permissionskit\E}{com/example/app}g; s/\Q:permissions:\E/:core:permissions:/g; s/"\Q:permissions\E"/":core:permissions"/g; s/projects\.\Qpermissions\E\./projects.core.permissions./g; s/libs\.plugins\.\Qpermissionskit\E\./libs.plugins.app./g' find core/permissions -depth -type d -path '*/io/thernal/permissionskit' | while read -r d; do mkdir -p "${d%/io/thernal/permissionskit}/com/example" && mv "$d" "${d%/io/thernal/permissionskit}/com/example/app" done find core/permissions -depth -type d -empty -delete
-
Provide what the copy expects — the
requireslist inkit.yml: convention plugins (build-kit's, or the ones in this repository'sbuild-logic/convention), catalog entries, settings — and, where listed, platform setup. -
The skill (optional): copy
skills/permissions-kitinto the app's skills directory (.claude/skills/for Claude Code), with the same renames, so an agent working in the app knows the kit. -
Updates are yours to carry:
git diff <the commit you copied> <a newer one> -- <the code paths>in the kit shows what changed; apply what you want, renamed the same way.
| Module | Holds | Depends on |
|---|---|---|
permissions/api |
AppPermission, PermissionStatus, PermissionState, MultiPermissionState, PermissionStateProvider, LocalPermissionStateProvider, rememberPermissionState |
Compose |
permissions/impl |
the shared status controller; the Android provider (launcher, request ledger); the iOS provider | api, lifecycle, activity-compose and core (Android) |
permissions/wiring |
the provider as a ProvidedValue in the app graph |
api, impl, Metro |
permissions/testing |
FakePermissionStateProvider |
api |
sample/shared, sample/android, sample/ios |
one screen asking for every permission, on both platforms | the kit — never copied |
./gradlew buildEvery target, tests on the JVM host and the iOS simulator — the controller (re-read before asking, asking
only what is missing, one prompt at a time), Android's status rules and manifest mapping, iOS's status
mappings, the fake — Detekt, which fails on any finding (-PdetektAutoCorrect=true fixes formatting
first), Android lint on the sample, and the sample's iOS framework link. The Gradle daemon runs on JDK 21
(Metro).
The iOS sample is an Xcode project (sample/ios/Sample.xcodeproj, generated from project.yml by
XcodeGen); its build phase compiles the framework with Gradle:
xcodebuild -project sample/ios/Sample.xcodeproj -scheme Sample -sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' build