{"id":19341413,"url":"https://github.com/eygraber/compose-permissionx","last_synced_at":"2026-02-16T11:14:04.326Z","repository":{"id":258373307,"uuid":"861427736","full_name":"eygraber/compose-permissionx","owner":"eygraber","description":"Better Android permission management with Compose UI","archived":false,"fork":false,"pushed_at":"2024-10-27T01:18:24.000Z","size":92,"stargazers_count":30,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-10-27T02:25:45.772Z","etag":null,"topics":["android","compose","compose-ui","permission","permission-android","ux"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eygraber.png","metadata":{"files":{"readme":"README.md","changelog":"changelog_config.json","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-22T21:22:35.000Z","updated_at":"2024-10-27T01:18:27.000Z","dependencies_parsed_at":"2024-10-18T18:04:57.339Z","dependency_job_id":null,"html_url":"https://github.com/eygraber/compose-permissionx","commit_stats":null,"previous_names":["eygraber/compose-permissionx"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eygraber%2Fcompose-permissionx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eygraber%2Fcompose-permissionx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eygraber%2Fcompose-permissionx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eygraber%2Fcompose-permissionx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eygraber","download_url":"https://codeload.github.com/eygraber/compose-permissionx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223909864,"owners_count":17223576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","compose","compose-ui","permission","permission-android","ux"],"created_at":"2024-11-10T03:30:36.922Z","updated_at":"2026-02-16T11:14:04.321Z","avatar_url":"https://github.com/eygraber.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Compose PermissionX\n\n[![Download](https://img.shields.io/maven-central/v/com.eygraber.permissionx/compose-permissionx/0.0.1)](https://search.maven.org/artifact/com.eygraber.permissionx/compose-permissionx)\n\n### Gradle\n\n```kotlin\nrepositories {\n  mavenCentral()\n}\n\nimplementation(\"com.eygraber.permissionx:compose-permissionx:0.3.0\")\n```\n\nSnapshots can be found [here](https://central.sonatype.org/publish/publish-portal-snapshots/#consuming-via-gradle).\n\n### Motivation\n\nHave you ever struggled with providing a good UX to your users when it comes time to ask them for permissions?\nDo you get the sense that Android makes it really difficult to do this?\n\nIf so, you're on to something. Requesting permission in Android is not as straightforward as it should be. With the\nbest of intentions around user safety, a monster was created. Unfortunately, the complicated nature of requesting\npermission from users usually results in a lot of frustration and corner cutting that leads to a poor UX, and a loss\nof trust from our users.\n\nThrough very careful state management, it is possible to get it right, but it comes with a lot of frustration.\n[Accompanist Permissions](https://google.github.io/accompanist/permissions/) goes a long way to solving that\nfrustration, and that is why Compose PermissionX is built on top of it.\n\nHowever, there are a couple of issues that Accompanist won't (or can't solve):\n\n1. `rememberPermissionState` breaks Compose Preview\n\n    - [An issue](https://github.com/google/accompanist/issues/1498) exists, but was closed,\n[delegating the responsibility](https://issuetracker.google.com/issues/267227895) for the fix to the\n`findActivity` API. While that might be the correct place to fix this issue, devs have had to deal with this for years,\nwith no movement towards a fix.\n\n    - Compose PermissionX solves this by simply making sure `findActivity` isn't called while in a preview.\n\n2. The default state of the permission before a request is made\n3. Detecting whether a permission was permanently denied\n\nBoth of these issues have the same underlying cause, namely the Android permissions framework doesn't want you to\nhave this information (allegedly for user safety, and so that it isn't simple to send users to the App Settings\nto grant the permission). It looks unlikely that this will ever\nchange\u003csup\u003e[1](https://github.com/google/accompanist/issues/1066)\u003c/sup\u003e\n\u003csup\u003e[2](https://github.com/google/accompanist/issues/1300)\u003c/sup\u003e\n\u003csup\u003e[3](https://github.com/google/accompanist/pull/990)\u003c/sup\u003e, and that's why Compose PermissionX is here.\n\n### Handling Canceled Permission Requests\n\nOne particularly tricky scenario occurs when a permission request is canceled. This can happen when:\n- The user taps outside the permission dialog\n- The system quickly denies the request for some reason\n- The permission was previously permanently denied in a prior app session\n\nIn these cases, Android returns a denied result without showing the rationale flag (`shouldShowRationale = false`).\nThis makes it **impossible to distinguish** between:\n1. A genuine permanent denial\n2. A canceled request\n\nCompose PermissionX solves this with a hybrid approach:\n\n#### First Request: Always Denied\n\nThe first request will always transition to `Denied`, even if the underlying system status indicates permanent denial.\nThis gives the consumer a chance to show rationale and request again:\n\n```\nNotRequested → Denied\n```\n\n#### Subsequent Requests: Timing Heuristic\n\nAfter the first request, Compose PermissionX uses a **timing threshold** to distinguish between cancellations and\npermanent denials:\n\n- If the result comes back **faster than 135ms** (configurable via `cancellationThreshold`), the system likely\ndidn't show a dialog (true permanent denial) → `PermanentlyDenied`\n- If the result takes **longer than 135ms**, the user likely saw and dismissed the dialog (cancellation) → `Denied`\n\n```\nDenied → (fast result) → PermanentlyDenied\nDenied → (slow result) → Denied (can request again)\n```\n\nThis threshold is configurable:\n\n```kotlin\nval permissionState = rememberPermissionState(\n  permission = android.Manifest.permission.CAMERA,\n  cancellationThreshold = 200.milliseconds, // custom threshold\n)\n```\n\nThe default of 135ms was chosen because it's slightly longer than typical system response times for pre-denied\npermissions, while being short enough that user interactions (even quick taps) will exceed it.\n\n### Usage\n\nMost of the API mirrors Accompanist Permissions.\n\nFor single permission requests:\n\n```kotlin\n@Composable\nfun FeatureThatRequiresCameraPermission() {\n  val cameraPermissionState = rememberPermissionState(\n    android.Manifest.permission.CAMERA\n  )\n\n  when(val status = cameraPermissionState.status) {\n    PermissionStatus.Granted -\u003e FeatureWithGrantedCameraPermission()\n    PermissionStatus.NotGranted -\u003e {\n      val message = when(status) {\n        PermissionStatus.NotGranted.NotRequested -\u003e\n          \"Camera permission required for this feature to be available. Please grant the permission.\"\n        \n        PermissionStatus.NotGranted.Denied -\u003e\n          \"The camera is important for this app. Please grant the permission.\"\n        \n        PermissionStatus.NotGranted.PermanentlyDenied -\u003e\n          \"This feature can't be used without granting the camera permission. Please grant it in the app's settings.\"\n      }\n      \n      val buttonLabel = when(status) {\n        PermissionStatus.NotGranted.PermanentlyDenied -\u003e \"Open App Settings\"\n        else -\u003e \"Request Permission\"\n      }\n      \n      Column {\n        Text(message)\n\n        Spacer(modifier = Modifier.height(8.dp))\n        \n        Button(\n          onClick = {\n            // alternatively call cameraPermissionState.launchPermissionRequestOrAppSettings()\n            when(status) {\n              PermissionStatus.NotGranted.PermanentlyDenied -\u003e cameraPermissionState.openAppSettings()\n              else -\u003e cameraPermissionState.launchPermissionRequest()\n            }\n          }\n        ) {\n          Text(buttonLabel)\n        }\n      }\n    }\n  }\n}\n```\n\nFor multiple permission requests:\n\n```kotlin\n@Composable\nfun FeatureThatRequiresCameraAndRecordAudioPermission() {\n  val multiplePermissionsState = rememberMultiplePermissionsState(\n    android.Manifest.permission.CAMERA,\n    android.Manifest.permission.RECORD_AUDIO,\n  )\n\n  when {\n    multiplePermissionsState.isAllPermissionsGranted -\u003e FeatureWithGrantedCameraAndRecordAudioPermission()\n    else -\u003e {\n      Text(multiplePermissionsState.getTextForDeniedPermissions())\n\n      Spacer(modifier = Modifier.height(8.dp))\n\n      val buttonLabel = when {\n        multiplePermissionsState.isAllNotGrantedPermissionsPermanentlyDenied -\u003e \"Open App Settings\"\n        else -\u003e \"Request Permissions\"\n      }\n      \n      Button(\n        onClick = {\n          // alternatively call multiplePermissionsState.launchMultiplePermissionRequestOrAppSettings()\n          when {\n            multiplePermissionsState.isAllNotGrantedPermissionsPermanentlyDenied -\u003e\n              multiplePermissionsState.openAppSettings()\n            \n            else -\u003e\n              multiplePermissionsState.launchMultiplePermissionRequest()\n          }   \n        }\n      ) {\n        Text(buttonLabel)\n      }\n    }\n  }\n}\n\nprivate fun MultiplePermissionsState.getTextForDeniedPermissions() = buildString {\n  if(isNotRequested) {\n    append(\n      \"Camera and record audio permissions are required for this feature to be available. Please grant the permissions.\"\n    )\n  }\n  else {\n    val isCameraPermanentlyDenied = isPermanentlyDenied(android.Manifest.permission.CAMERA)\n    val isRecordAudioPermanentlyDenied = isPermanentlyDenied(android.Manifest.permission.RECORD_AUDIO)\n\n    if(isCameraPermanentlyDenied \u0026\u0026 isRecordAudioPermanentlyDenied){\n      append(\"This feature can't be used without granting the camera and record audio permissions. \")\n      append(\"Please grant them in the app's settings.\")\n    }\n    else {\n      val isCameraDenied = isDenied(android.Manifest.permission.CAMERA)\n      val isRecordAudioDenied = isDenied(android.Manifest.permission.RECORD_AUDIO)\n\n      append(\"The \")\n\n      if(isCameraDenied \u0026\u0026 isRecordAudioDenied) append(\"camera and record audio permissions are \")\n      else if(isCameraDenied) append(\"camera permission is \")\n      else if(isRecordAudioDenied) append(\"record audio permission is \")\n\n      append(\"important for this app. Please grant the \")\n      if(isCameraDenied \u0026\u0026 isRecordAudioDenied) append(\"permissions.\")\n      else append(\"permission.\")\n    }\n  }\n}\n```\n\n### KMP\n\nCurrently, this library just supports Android. If there is ever a need to solve a platform specific issue on other\nplatforms it can get added here. It is not a goal of Compose PermissionX to be a general use case multiplatform\npermission solution.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feygraber%2Fcompose-permissionx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feygraber%2Fcompose-permissionx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feygraber%2Fcompose-permissionx/lists"}