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
orREAD_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
Call
PermissionHandler()
at the top level of your screen or root Composable.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! 🎉
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