Requesting Android Permissions With Example
This post is a reference for how I created a permission requesting system in Android Compose.

In this post I am going to show you how I created my method for requesting Android permissions in Compose.

Requesting permissions is necessary to be able to access parts of the Android system in your app. Either hardware or software.

There has continued to be quite a bit of changes over the years to requesting permissions in Android as the OS keep improving security. They are all designed toward keeping the end user safe and knowledgeable about how the app functions.

My method I will show you how to create a process of:

  • Asking for permission
  • If user denies permission more than once, inform the user the permission is needed for app functionality and ask them to go into settings.
  • Return back from the settings and update the UI based on permission change

Steps

AndroidManifest.xml

Add permission you want to the AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
    ....

MainActivity

This is what I added to the MainActivity class. It is a Composable function that will store the permission requesting methods added to the Main Activity if the state of the permission is false that is loaded from the ViewModel.

You should hold the states of the permission somehow to be able to launch the coroutine methods of permission request, dialog requests and settings request. I chose to use the ViewModel but you can use single remember{ mutableStateOf(false)}.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val factory = ViewModelFactory()
        val viewModel = ViewModelProvider(
            this,
            factory
        )[ViewModel::class.java]

        setContent {
            SyncOurListsTheme {

                val permissionState =
                    viewModel.post_permissionState.collectAsStateWithLifecycle().value
                val requestingPermissions = listOf(
                    Manifest.permission.POST_NOTIFICATIONS
                )
                val check = ContextCompat.checkSelfPermission(
                    applicationContext,
                    requestingPermissions[0]
                ) == PackageManager.PERMISSION_GRANTED
                viewModel.changePostPermission(check)

                Scaffold(modifier = Modifier.fillMaxSize(), topBar = {
                }) { innerPadding ->
                    if (!permissionState) {
                        Row(modifier = Modifier.padding(20.dp), horizontalArrangement = Arrangement.Center) {
                            MyPermissionState(
                                permissionState,
                                requestingPermissions =
                                requestingPermissions,
                                callback = {
                                    viewModel.changePostPermission(it)
                                }
                            )
                        }
                    }
                    Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(Color.Black), contentAlignment = Alignment.Center){
                        Text(
                            text = "Message = \n${if (permissionState) "Permission Granted" else "Permission Denied"}",
                            color = if (permissionState) Color.Green else Color.Red
                        )
                    }

                }
            }
        }
    }
}

ViewModelFactory

This is my ViewModelFactory class.

class ViewModelFactory() : ViewModelProvider.Factory {

    //------------to create a ViewModel. This will ensure same ViewModel is used
    //val factory = ViewModelFactory()
    //val viewModel = ViewModelProvider(this, factory)[ViewModel::class.java]//by viewModels<ViewModel>()

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return if(modelClass.isAssignableFrom(com.itgeek25.syncourlists.viewmodel.ViewModel::class.java)){
            ViewModel() as T
        } else {
            throw IllegalArgumentException("ViewModel Not Found!!")
        }
    }
}

ViewModel

This is my ViewModel class.

class ViewModel() : ViewModel() {

    private val _post_permissionState: MutableStateFlow<Boolean> = MutableStateFlow(false)
    val post_permissionState: StateFlow<Boolean> = _post_permissionState.asStateFlow()

    fun changePostPermission(isGranted : Boolean){
        viewModelScope.launch {
            _post_permissionState.value = isGranted
        }
    }

}

MyPermissionState

Now this is the function I used to do all the permission work. I created a class for the function and named it MyPermissionState.

@Composable
fun MyPermissionState(
    permissionState : Boolean,
    requestingPermissions: List<String>,
    callback : (Boolean) -> Unit
){

    val context = LocalContext.current

    var showRationaleState by remember { mutableStateOf(false) }
    var settingsState by remember { mutableStateOf(false) }

    val requestPermissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){ permissions ->
        permissions.forEach { (name, state) ->
            when(name){
                requestingPermissions[0] -> {
                    Log.i("Permissions", "$name : $state")
                    callback(state)
                }
            }
        }
    }

    val goToSettingsRequest = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()){ activityResult ->
    }

    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(key1 = lifecycleOwner) {
        val observer = LifecycleEventObserver{ _, event ->
            if(event == Lifecycle.Event.ON_RESUME){
                val check = ContextCompat.checkSelfPermission(context, requestingPermissions[0]) == PackageManager.PERMISSION_GRANTED
                callback(check)
                if(check){
                    showRationaleState = false
                    settingsState = false
                }
            }
            if(event == Lifecycle.Event.ON_CREATE){
                when {
                    permissionState -> {
                        //permission granted
                    }
                    ActivityCompat.shouldShowRequestPermissionRationale(context as Activity, requestingPermissions[0]) -> {
                        //show rationale dialog
                        showRationaleState = true
                    }
                    else -> {
                        //no permission, request it
                        requestPermissions.launch(requestingPermissions.toTypedArray())
                    }
                }

            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    LaunchedEffect(settingsState) {
        if(settingsState){
            settingsState = false
            goToSettingsRequest.launch(
                Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
                    Uri.fromParts("package", context.packageName, null)
                ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            )

        }
    }

    if(showRationaleState) {
        AlertDialog(
            onDismissRequest = {
                showRationaleState = false
            },
            text = {
                Text(text = "Permission Not Granted. Please go to app settings and grant access for app functionality.")
            },
            confirmButton = {
                TextButton(onClick = {
                    goToSettingsRequest.launch(
                        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
                            Uri.fromParts("package", context.packageName, null)
                        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    )
                }) {
                    Text(text = "Open App Settings")
                }
            }
        )
    }
}

How It Works

This is mostly state driven. There are probably easier ways to do this but I wanted a way that I could reuse the same process over and over without the need to alter much code for different projects.

Breakdown

  • The ViewModel holds the state of permission. When the app starts, the MainActivity has a method that checks if the permission is already granted. The value is passed to the ViewModel to hold onto.
  • If the check value is access denied, then the MyPermissionState function is loaded.
  • Inside the MyPermissionState function, there is a Disposable function that calls when the Lifecycle event of ON_CREATE is called. Inside this call, the permission is checked again then passed to a when{} statement. This is basically an if/else statement.
  • This checks for permission granted OR if user should be shown a rational reasoning dialog as to why the permission is needed ( this happens after the user has denied access ) OR permission is not granted yet ( start the asking process ).
  • After requesting permission, if the user selects to deny access. Then the user would need to be shown a dialog with an explanation of why the permission is needed in order to ask again. This is the flow the newer permission system is designed for. The idea is to not continually request permissions which creates a bad user experience.
  • The rational dialog will now display with the explanation of why the permission is needed along with a button to send the user to the app settings so they can change it if they desire.
  • The ON_RESUME function inside the DisposableEffect is to capture the change from the user going into the settings than navigating back to the app. It will again check for permission access and send this to the callback function back to the MainActivity. In the MainActivity, it will receive this and update the permission state held in the ViewModel.

BONUS

WITHOUT USING VIEWMODEL

Here is a sample of the same implementation without utilizing a ViewModel. The only thing changed was the MainActivity class.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            SyncOurListsTheme {
                val requestingPermissions = listOf(
                    Manifest.permission.POST_NOTIFICATIONS
                )
                var permissionState by remember { mutableStateOf(ContextCompat.checkSelfPermission(
                    applicationContext,
                    requestingPermissions[0]
                ) == PackageManager.PERMISSION_GRANTED) }

                //viewModel.changePostPermission(check)

                Scaffold(modifier = Modifier.fillMaxSize(), topBar = {
                }) { innerPadding ->
                    if (!permissionState) {
                        Row(modifier = Modifier.padding(20.dp), horizontalArrangement = Arrangement.Center) {
                            MyPermissionState(
                                permissionState,
                                requestingPermissions =
                                requestingPermissions,
                                callback = {
                                    permissionState = it
                                }
                            )
                        }
                    }
                    Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(Color.Black), contentAlignment = Alignment.Center){
                        Text(
                            text = "Message = \n${if (permissionState) "Permission Granted" else "Permission Denied"}",
                            color = if (permissionState) Color.Green else Color.Red
                        )
                    }

                }
            }
        }
    }
}

I hope this helps anyone navigate through this process easier.

Leave a Reply

Your email address will not be published. Required fields are marked *