Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 041 / 100

Permissions in Android: a practical guide for developers

Request permissions the right way.

Expona Admin6 min read

Permissions in Android: a practical guide for developers

Handling permissions is one of those things that feels straightforward until you ship to production and discover users never granted access to the camera, location, or contacts. Getting permissions wrong means your features silently fail, support tickets pile up, and users uninstall. In this guide, we'll walk through the modern approach to requesting and managing permissions so your app behaves predictably across Android versions while respecting user privacy.

Why permissions matter

Android's permission model evolved significantly from API level 1 to today. Pre-Android 6.0 (API 23), users granted all permissions at install time. Starting with API 23, we shifted to runtime permissions—users now grant permissions when your app actually needs them. This shift gives users fine-grained control and forces us as developers to be intentional about what we request and when.

Mishandling permissions leads to crashes, silent failures, and poor user experience. The good news: modern Android provides solid tools to do this right. Let's cover the essentials.

Declare in manifest — Only what you need

Every permission your app uses must be declared in AndroidManifest.xml. This is non-negotiable. The manifest is your contract with the system and users about what your app might do.

The key principle: declare only what your app genuinely needs. Bloated permission lists make users suspicious and may hurt app store rankings. Common dangerous permissions include CAMERA, ACCESS_FINE_LOCATION, READ_CONTACTS, and READ_CALL_LOG.

Here's a realistic scenario: you're building a photo editing app. You need camera access to let users capture photos and storage access to read and save edited images.

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

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

    <application>
        ...
    </application>
</manifest>

Notice we declared specific media permissions rather than the older READ_EXTERNAL_STORAGE. Permissions evolve; keep your target SDK current and use the latest permission names.

If you're targeting Android 12 or later and need approximate location instead of precise, declare ACCESS_COARSE_LOCATION instead of (or in addition to) ACCESS_FINE_LOCATION. Users appreciate the choice.

Request at runtime — Ask when the feature is used, not at launch

Runtime permissions are requests that happen while the app is running, not at install time. This is where many teams stumble: they ask for everything on launch, frustrating users who haven't even reached the feature yet.

Best practice: request permissions only when the user tries to use the feature that needs it.

Let's say your photo app has a "Capture Photo" button. Don't ask for camera permission when the app starts. Ask when the user taps that button.

kotlin
class PhotoCaptureFragment : Fragment() {

    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            openCamera()
        } else {
            showPermissionDeniedUI()
        }
    }

    private fun onCapturePhotoClicked() {
        when {
            ContextCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED -> {
                openCamera()
            }
            shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
                showExplanationDialog()
            }
            else -> {
                requestPermissionLauncher.launch(Manifest.permission.CAMERA)
            }
        }
    }

    private fun openCamera() {
        // Launch camera intent or fragment
    }

    private fun showPermissionDeniedUI() {
        Toast.makeText(
            requireContext(),
            "Camera permission is required to capture photos",
            Toast.LENGTH_SHORT
        ).show()
    }
}

This flow checks three states: permission already granted, permission previously denied but rationale should be shown, or permission not yet requested. Modern Activity Result APIs (via registerForActivityResult) are cleaner than the older requestPermissions callback approach and handle lifecycle edge cases automatically.

For multiple permissions, use RequestMultiplePermissions:

kotlin
private val multiPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { permissions: Map<String, Boolean> ->
    val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
    val locationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false

    when {
        cameraGranted && locationGranted -> proceedWithFeature()
        cameraGranted -> showPartialPermissionUI()
        else -> showPermissionDeniedUI()
    }
}

private fun requestMultiplePermissions() {
    multiPermissionLauncher.launch(
        arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.ACCESS_FINE_LOCATION
        )
    )
}

Explain why — Show rationale if the user declined before

Users often deny permissions reflexively. If they deny once, your app should explain why it needs that permission before asking again. This is where the shouldShowRequestPermissionRationale API comes in.

The logic is: if the user has denied a permission and your app is asking again, the system will return true for shouldShowRequestPermissionRationale. This signals that you should show an explanation before requesting again.

In our photo app, maybe the user dismissed the camera permission request. The next time they tap "Capture Photo," we should show a dialog explaining why we need it:

kotlin
private fun showExplanationDialog() {
    AlertDialog.Builder(requireContext())
        .setTitle("Camera Permission Required")
        .setMessage(
            "We need camera access to let you capture photos. " +
            "Without it, the photo capture feature won't work."
        )
        .setPositiveButton("Grant") { _, _ ->
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
        .setNegativeButton("Cancel", null)
        .show()
}

This explanation should be honest and concise. Don't oversell or guilt-trip users. Explain the functional benefit, nothing more.

If the user denies a second time and shouldShowRequestPermissionRationale returns false, they've likely chosen "Don't ask again." At this point, direct them to app settings where they can enable the permission manually:

kotlin
private fun openAppSettings() {
    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
        data = Uri.fromParts("package", requireContext().packageName, null)
    }
    startActivity(intent)
}

Handle denial — Offer a degraded experience

Users have the right to deny permissions. Your app must not crash; it should offer a graceful fallback.

In our photo editor, if the user denies camera permission, we can still let them import photos from storage. If they deny storage permission too, we might offer cloud import or let them paste images. The point: core functionality should degrade gracefully, not disappear entirely.

kotlin
private fun handleCameraPermissionDenied() {
    // Show UI with alternative options
    binding.capturePhotoButton.isEnabled = false
    binding.importPhotoButton.isEnabled = true
    binding.permissionWarning.apply {
        visibility = View.VISIBLE
        text = "Camera permission denied. You can still import photos from your library."
    }
}

private fun onImportPhotoClicked() {
    // Don't require camera permission
    val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
    startActivity(intent)
}

This is better UX than disabling the entire app or showing a one-time permission error and giving up.

Common mistakes

Asking for permissions at app launch: Requesting all permissions upfront frustrates users. Request only when needed.

Ignoring permission denial: Not handling the case where a permission is denied leads to crashes. Always check before using a protected API.

Declaring unnecessary permissions: Every permission in your manifest should have a purpose. Remove old permissions after refactoring features.

Using deprecated permission names: The Android permission model evolves. If targeting Android 13+, use READ_MEDIA_IMAGES instead of READ_EXTERNAL_STORAGE.

Not testing on multiple API levels: Permission behavior differs between Android versions. Test on at least API 24 and your target SDK.

Overexplaining in rationale: Keep permission explanations brief. Users have already decided once; respect their intelligence.

Key takeaways

  • Declare permissions in the manifest, but only those your app genuinely needs.
  • Request permissions at runtime when the user attempts to use the feature, not at app launch.
  • Check permission status before making a request; use shouldShowRequestPermissionRationale to decide whether to explain first.
  • Use modern Activity Result APIs (registerForActivityResult) rather than deprecated callback methods.
  • Always handle permission denial gracefully; offer degraded experiences rather than crashing or giving up entirely.
  • Keep rationale messages concise and honest.
  • Test on multiple Android API levels and real devices; simulator behavior can mask permission issues.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles