Requesting Location Permission in Jetpack Compose

Requesting Location Permission in Jetpack Compose

Requesting permissions has been an important workflow when building our apps to access the different features on the Android device. This usually is done through invoking requestPermissions . If you are new to Android, this article is a good place to start.

With Jetpack Compose there are some changes in this approach and how we can request permissions. In this post, I will show you the approach I took to handle permissions in my app, that is being redesigned with Jetpack Compose.

Requesting permissions in Android needs to follow the below workflow as described in the official documentation:

Before I started to build my own Composable to handle permissions, I tried to see how the community is implementing it. But I was not able to find a straight forward approach. Accompanist does have Permissions component that we could leverage but it's still experimental at the time of this writing.

I would suggest taking a look at this Composable as it's maintained by the Google developers, might have more features and also be stable by the time you are reading this.

Here is the overview of this process:

Declaring Permissions

Before we can request permissions, we need to declare them in the manifest.  To declare location permission, we need to add the below:

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Design your App UX for user action

Within our app we would need to perform an action that triggers the permission request. This is usually done by the user clicking on a Button, FAB etc. In my app, I have a FAB on the Locations screen, to request a snapshot of the user's current location. This is useful when you are riding in a car as a passenger and see something interesting and want a quick capture of the area for review later.

Check If Permission Already Granted

When the user clicks on the button, we need to check if the permission has already been granted. This is false the first time the user installs the app or if the user has previously granted a one time permission.

val context = LocalContext.current

val permissionGranted = Common.checkIfPermissionGranted(
            context,
            permission//Manifest.permission.ACCESS_FINE_LOCATION
        )     
PermissionsUI.kt

If the permission has been granted, we than continue with requesting the location information.

if (permissionGranted) {
        permissionAction(PermissionAction.OnPermissionGranted)
        return
}
PermissionsUI.kt

PermissionAction is a sealed class that we defined to expose the different events that happen in our Permissions composable. These events can be passed to the parent composable to perform the desired actions or modify the state.

sealed class PermissionAction {

    object OnPermissionGranted : PermissionAction()

    object OnPermissionDenied : PermissionAction()
}
PermissionAction.kt

permissionAction is a parameter on the PermissionsUI composable that returns an Unit permissionAction: (PermissionAction) -> Unit

Check If Rationale Needs to Be Shown to the User

If the permission is not already granted, we need to check if a rationale needs to be presented to the user, explaining why the permission needs to be granted.

  val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {    
            // Permission Accepted
            permissionAction(PermissionAction.OnPermissionGranted)
        } else {
            // Permission Denied
            permissionAction(PermissionAction.OnPermissionDenied)
        }
    }
    
    
    val showPermissionRationale = Common.shouldShowPermissionRationale(
        context,
        permission
    )
fun shouldShowPermissionRationale(context: Context, permission: String): Boolean {

            val activity = context as Activity?
            return ActivityCompat.shouldShowRequestPermissionRationale(
                activity!!,
                permission
            )
}
Common.kt

If the rationale does not need to be shown, launch the permission request from the Composable. We do this by launching the ActivityResultLauncher returned from rememberLauncherForActivityResult for ActivityResultContracts.RequestPermission()

SideEffect {
  launcher.launch(permission)
}

Since launching the activity changes the state of the app outside the current scope of the composable function, we need to wrap it up in SideEffect to safely launch it.

The user will be presented with the below screen to grant location permission

If the rationale needs to be shown, we than present the user with a Snackbar or similar UI element.

Showing Permission Rationale to User

We can show the rationale by using a Snackbar and than identifying the result of the Snackbar to pass the PermissionAction to the parent composable function.

if (showPermissionRationale) {
  LaunchedEffect(showPermissionRationale) {
    val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                message = permissionRationale,
                actionLabel = "Grant Access",
                duration = SnackbarDuration.Long
            )
            when (snackbarResult) {
                SnackbarResult.Dismissed -> {
                    //User denied the permission, do nothing
                    permissionAction(PermissionAction.OnPermissionDenied)
                }
                SnackbarResult.ActionPerformed -> {                    
                    launcher.launch(permission)
                }
            }
        }
    }

This would show the below Snackbar to the user. If user does not perform any action, the SnackbarResult would be Dismissed

If you want to see how to configure Snackbar with Jetpack Compose, refer to other my post.

Putting It All Together

If we combine all the code into a Composable function that could be also called from other Composable functions we would  have the below:

@Composable
fun PermissionUI(
    context: Context,
    permission: String,
    permissionRationale: String,
    scaffoldState: ScaffoldState,
    permissionAction: (PermissionAction) -> Unit
) {


    val permissionGranted =
        Common.checkIfPermissionGranted(
            context,
            permission
        )

    if (permissionGranted) {
        Log.d(TAG, "Permission already granted, exiting..")
        permissionAction(PermissionAction.OnPermissionGranted)
        return
    }


    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            Log.d(TAG, "Permission provided by user")
            // Permission Accepted
            permissionAction(PermissionAction.OnPermissionGranted)
        } else {
            Log.d(TAG, "Permission denied by user")
            // Permission Denied
            permissionAction(PermissionAction.OnPermissionDenied)
        }
    }


    val showPermissionRationale = Common.shouldShowPermissionRationale(
        context,
        permission
    )


    if (showPermissionRationale) {
        Log.d(TAG, "Showing permission rationale for $permission")

        LaunchedEffect(showPermissionRationale) {

            val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                message = permissionRationale,
                actionLabel = "Grant Access",
                duration = SnackbarDuration.Long

            )
            when (snackbarResult) {
                SnackbarResult.Dismissed -> {
                    Log.d(TAG, "User dismissed permission rationale for $permission")
                    //User denied the permission, do nothing
                    permissionAction(PermissionAction.OnPermissionDenied)
                }
                SnackbarResult.ActionPerformed -> {
                    Log.d(TAG, "User granted permission for $permission rationale. Launching permission request..")
                    launcher.launch(permission)
                }
            }
        }
    } else {
        //Request permissions again
        Log.d(TAG, "Requesting permission for $permission again")
        SideEffect {
            launcher.launch(permission)
        }

    }


}
PermissionUI.kt

Invoking the PermissionUI Composable from another Composable

To invoke our composable function, we should be able to call it based on state change.

I use a MutableStateFlow Boolean variable called requestLocationPermission in my ViewModel, that is set to true only when the FAB is clicked and set to false regardless of the outcome of the PersmissionAction. Might not be the ideal way to do it and definitely open to improvements.

val context = LocalContext.current
val requestLocationPermission by locationViewModel.requestLocationPermission.collectAsState()
    
    if (requestLocationPermission) {
        PermissionUI(
            context,
            Manifest.permission.ACCESS_FINE_LOCATION,
            stringResource(id = R.string.permission_location_rationale),
            scaffoldState
        ) { permissionAction ->

            when (permissionAction) {
                is PermissionAction.OnPermissionGranted -> {
                    Log.d(TAG, "Permission grant successful"){                   
                   locationViewModel.setRequestLocationPermission(false)
                    
                    //To Do: Request User Location 



                }
                is PermissionAction.OnPermissionDenied -> {
                    Log.d(TAG, "Permission grant denied")
                    locationViewModel.setRequestLocationPermission(false)
                                           
         
                }
            }

        }
    }
LocationsUI.kt

ViewModel code for declaring the Boolean flow

@HiltViewModel
class LocationViewModel @Inject constructor(
    private val locationRepository: LocationRepository,
    private val userPreferencesRepository: UserPreferencesRepository
) :
    ViewModel() {


...

   private val _requestLocationPermission: MutableStateFlow<Boolean> =  MutableStateFlow(false)

    val requestLocationPermission = _requestLocationPermission.asStateFlow()


    fun setRequestLocationPermission(request: Boolean) {
        _requestLocationPermission.value = request
    }
    
...

}
LocationViewModel.kt

Let me know your thoughts on this approach in the comments below. If you have any ideas on how to improve this, I'm all ears!

You can check out the source code of a working example with the above concepts here.