Kotlin Lightweight Android permissions library that follows the permission request principles and its Jetpack Compose friendly.
Learn more about best practices at https://developer.android.com/guide/topics/permissions/overview
Disclaimer: This is not an officially supported Google product, it's an experimental personal project, the API is constantly changing, use at your own risk.
This library provides a wrapper around the existing Jetpack Activity Contracts to solve the following problems:
- Android Lifecycle
- Abstraction and testability
- Permission rejection
This is done with the combination of Jetpack Startup and the Jetpack Activity and Fragment KTX by abstracting the access to the Permission status and enforcing best practices to improve permission acceptance rate.
- How to include in your project
- Check Permission Status
- Register Permission Request
- Launch Permission Request
- Multiple permissions launch
- Observe Permission Status
- Self Initialization
- Testing
The library is available via MavenCentral:
allprojects {
repositories {
// ...
mavenCentral()
}
}
Add it to your module dependencies:
dependencies {
// base module
implementation 'dev.marcelpinto:permissions-ktx:$version'
// for compose projects
implementation 'dev.marcelpinto:permissions-compose-ktx:$version'
}
The Permission inline class provides type safety and access to quickly check the status of a given permission:
val finePermission = Permission(Manifest.permission.ACCESS_FINE_LOCATION)
when (val status = finePermission.status) {
is PermissionStatus.Granted -> // Do something
is PermissionStatus.Revoked -> if (status.rationale == PermissionRational.REQUIRED) {
// Show something
} else {
// Do something else
}
}
The library follows the same mechanism as ActivityResultContracts by registering in your Fragment or Activity a variable for result but instead of using registerForActivityResult() you should use registerForPermissionResult(permissionName)
class MainFragment : Fragment() {
private val locationPermissionRequest =
registerForPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION) { granted ->
// do something when permission is granted or rejected
}
}
This creates a PermissionRequest instance that can be used to launch the permission request flow.
There are two ways to launch a permission request:
This is the desired way to launch since it enforces the permission recommendation flow by:
- Checking if the permission was already granted --> onAlreadyGranted
- Then if further explanation is required --> onRequireRational
- Otherwise launching the permission request --> onRequirePermissions
locationPermissionRequest.safeLaunch(
onRequirePermissions = {
// Optional:update your UI if needed and return true to launch
// the permission request
true
},
onRequireRational = {
// Show a rational (i.e snackbar/dialog) and call
// locationPermissionRequest.launch() if user acknowledges
// the rational
},
onAlreadyGranted = {
// Optional: perform action since permission was already granted
}
)
Note: only
onRequireRational
lambda is required.
Check the Simple sample or the Compose Sample for more.
For other case (or for backwards compatibility with your existing code) where you
want to launch directly the permission request, the launch()
method provided
by the Jetpack Activity/Fragment library can still be used.
// this will launch the Android Permission request directly
locationPermissionRequest.launch()
The library support launching multiple permissions at the same time, although this is only encourage for specific cases, for example, a videochat (CAMERA and MIC). Otherwise is always better to request only the necessary single permission for each use case, instead of requesting all at once.
To launch multiple permissions simply pass an array following the same mechanism explained above:
locationPermissionsRequest = registerForMultiplePermissionResult(
arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
) { resultMap ->
// a map of Permission and the result as boolean.
}
Check the MultipleActivity sample.
The library adds an observability pattern to the current Android Permissions API by providing a Flow that emits every time a declared permission status changes.
This can be used to update UI in a reactive way or to enable/disable certain APIs that requires a permission (i.e LocationManager).
val finePermission = Permission(Manifest.permission.ACCESS_FINE_LOCATION)
lifecycleScope.launch {
finePermission.statusFlow.collect { status ->
// Based on the status update UI or enable/disable another component
// that requires the permission
}
}
Note: for an example of this check the Advance sample
The library uses the Jetpack Startup library to self-initialize in the right moment.
In case you want to use your own mechanism you can initialize it by calling:
PermissionProvider.init(context)
And disabling the self-initialization on you AndroidManifest.xml adding the following tag:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup">
<meta-data
android:name="dev.marcelpinto.permissionktx.PermissionInitializer"
android:value="androidx.startup"
tools:node="remove"/>
</provider>
The library is built with testability in mind to ensure that the permission flow can be tested without Android dependencies and it's fully controllable.
For Unit Testing the library provides an overload of the Permission.init method that allows to provide custom implementation of the Permission.Checker and Permission.Observer allowing the test to control the status of the permission without Android dependencies.
// Using a StateFlow to change the values provided by the Observer and Checker
// You could use other mechanisms or directly a simple variable
private var permissionStatus = MutableStateFlow<Permission.Status>(
Permission.Status.Revoked(
type = Permission(Manifest.permission.ACCESS_FINE_LOCATION),
rationale = Permission.Rational.OPTIONAL
)
)
@Before
fun setUp() {
val checker = object : Permission.Checker {
// Returns the defined value in our StateFlow variable
override fun getStatus(type: Permission) = permissionStatus.value
}
val observer = object : Permission.Observer {
override fun getStatusFlow(type: Permission) = permissionStatus
override fun refreshStatus() {
permissionStatus.value = permissionStatus.value
}
}
// Override the Permission initialization with the "fake" implementations
PermissionProvider.init(checker, observer)
}
@Test
fun test() {
// Emit new PermissionStatus to the permissionStatus flow to
// test different scenarios
}
Check the AdvanceViewModelTest for a complete example
To allow control of the permission flow without having to grant/revoke Android permissions the library provides an overload of the Permission.Init method that allows to provide custom implementation for Checker, Observer and the ActivityResultRegistry to use when launching the permission request (see Testing ActivityResult).
This allow full control and customization of the Permission status and permission request results, allowing to fully test the permission flow without interacting with the Android framework.
private var permissionStatus: Permission.Status = Permission.Status.Revoked(
type = Permission(Manifest.permission.ACCESS_FINE_LOCATION),
rationale = Permission.Rational.OPTIONAL
)
@Before
fun setUp() {
// Provide a custom init that returns the values of the defined permissionStatus
// and when request is launched it returns true or false depending on the permissionStatus
PermissionProvider.init(
context = InstrumentationRegistry.getInstrumentation().targetContext,
checker = object : Permission.Checker {
override fun getStatus(type: Permission) = permissionStatus
},
registry = object : ActivityResultRegistry() {
override fun <I, O> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, permissionStatus.isGranted())
}
}
)
}
Check the SimpleActivityTest for a complete example.