Handle permissions like a pro — effortlessly

Working with permissions in Android is crucial, but it can also be a bit of a headache — especially when you have to juggle initial permissions, runtime permissions, and must-have permissions that your app simply can't function without.

So to make your life easier, I built a plug-and-play Composable you can drop into your project to handle all of this for you. 👨‍💻👩‍💻


🛠 Permissions Covered

Initial Permissions (requested on first launch):

  • CAMERA

  • RECORD_AUDIO

  • POST_NOTIFICATIONS

  • READ_MEDIA_AUDIO (Must-have permission)

Runtime Permission (requested when needed):

  • READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES (based on Android version)

🔒 The app requires READ_MEDIA_AUDIO to function, so if it’s denied, we prompt the user to go to settings and enable it manually.


📋 AndroidManifest.xml

Make sure to declare all required permissions and hardware features:

xmlCopyEdit<uses-feature android:name="android.hardware.camera" android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

🔧 PermissionHandler.kt

Create a reusable Composable to handle all initial permissions and show a dialog for must-have ones:

@Composable
fun PermissionHandler() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    var showDialog by remember { mutableStateOf(false) }
    var permissionRequested by remember { mutableStateOf(false) }
    var initialPermissionsGranted by remember { mutableStateOf(false) }

    val initialPermissions = listOf(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO,
        Manifest.permission.POST_NOTIFICATIONS,
        Manifest.permission.READ_MEDIA_AUDIO
    )

    val initialLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestMultiplePermissions()
    ) { results ->
        initialPermissionsGranted = results.all { it.value }

        val audioGranted = results[Manifest.permission.READ_MEDIA_AUDIO] == true
        showDialog = !audioGranted
        permissionRequested = true
    }


    DisposableEffect(Unit) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_RESUME && permissionRequested) {
                val audioGranted = ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.READ_MEDIA_AUDIO
                ) == PackageManager.PERMISSION_GRANTED

                showDialog = !audioGranted
                initialPermissionsGranted = initialPermissions.all {
                    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
                }
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

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

    LaunchedEffect(Unit) {
        initialLauncher.launch(initialPermissions.toTypedArray())
    }

    if (initialPermissionsGranted) {
        Text("All required permissions granted")
    }

    if (showDialog) {
        AlertDialog(
            onDismissRequest = {},
            confirmButton = {
                TextButton(onClick = {
                    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                        data = Uri.fromParts("package", context.packageName, null)
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    }
                    context.startActivity(intent)
                }) {
                    Text("Go to Settings")
                }
            },
            dismissButton = {
                TextButton(onClick = { showDialog = false }) {
                    Text("Cancel")
                }
            },
            title = { Text("Permission Required") },
            text = {
                Text("This app needs access to Media Audio to work properly. Please enable it in Settings.")
            }
        )
    }
}

🧪 Runtime Permission Example

Request a runtime permission (like external storage access) on demand inside your screen:

@Composable
fun SampleScreenUI(modifier: Modifier = Modifier) {
    val context = LocalContext.current

    val storagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        Manifest.permission.READ_MEDIA_IMAGES
    } else {
        Manifest.permission.READ_EXTERNAL_STORAGE
    }

    val storageLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { granted ->
        val message = if (granted) {
            "Storage permission granted"
        } else {
            "Storage permission denied"
        }
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }

    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(onClick = { storageLauncher.launch(storagePermission) }) {
            Text("Request Storage Permission")
        }
    }
}

🧩 How to Use

  1. Call PermissionHandler() at the top level of your screen or root Composable.

  2. For runtime permissions, use rememberLauncherForActivityResult() in the Composable where you need them.

In the main activity you can easily call the PermissionHandler() for the initial permissions like this

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            PermissionHandler() // Calls the PermissionHandler in the first launch
            PermissionHandlingTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    SampleScreenUI()
                }
            }
        }
    }
}

That’s it — you now have a clean, reusable way to handle all kinds of permissions in your Jetpack Compose app! 🎉

0
Subscribe to my newsletter

Read articles from Sagnik Mukherjee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sagnik Mukherjee
Sagnik Mukherjee

Native Android Developer and content creator