Skip to content
All posts
September 10, 20266 min read

Android Runtime Permissions with the Activity Result API

A hands‑on guide to replacing legacy permission flows with the modern Activity Result API, covering migration steps, code samples, and best practices for secure app development.

AndroidKotlinSecurityBest Practices
Share:

The Pain of Legacy Permission Requests

If your app still asks for camera or location permission the old way, you’re fighting a battle that Android designed to be easier. The classic

code
requestPermissions()
method forces you to manage a separate callback, juggle activity lifecycle events, and still deal with the infamous “Never ask again” scenario. Developers who stick with the legacy flow often see higher crash rates and lower user consent because the UX feels abrupt and error‑prone. Switching to the Activity Result API eliminates these headaches and gives you a predictable, testable way to request permissions.

Why Runtime Permissions Still Matter

Android’s permission model separates dangerous permissions (camera, location, contacts) from normal ones to protect user privacy. Since Android 6.0 (Marshmallow), apps must ask for these permissions at runtime, not just in the manifest. The goal is clear: give users transparent control over what data their apps can access. However, the implementation has evolved. The original

code
PermissionManager.requestPermissions()
works, but it lives outside the fragment/activity result contract system, making it harder to integrate with modern Jetpack Compose workflows and to handle configuration changes gracefully.

FeatureLegacy
code
requestPermissions()
Activity Result API (
code
registerForActivityResult
)
Lifecycle safetyRequires manual handling of config changesAutomatically re‑registered after process restart
Result handling
code
onRequestPermissionsResult()
callback
code
ActivityResultLauncher
callback with
code
ActivityResult
TestingNeeds mock
code
Activity
or
code
Instrumentation
Can mock
code
ActivityResultLauncher
directly
UX consistencyVaries per API levelUniform across API levels, works with
code
ActivityResultContracts
Backward compatibilityWorks on all supported APIsRequires API 33+ for
code
ActivityResultContracts.GetPermission
(fallback available)

[!NOTE] The table above shows why the Activity Result API is the preferred path for new code. It aligns with Jetpack Compose’s declarative nature and reduces boilerplate.

Migrating to the Activity Result API

The Activity Result API was introduced as part of AndroidX Activity library and later expanded to include permission contracts. The key is to pair

code
registerForActivityResult
with a
code
ActivityResultContract
that defines the intent and result parsing. For permissions,
code
GetPermission
is the contract you need.

Below is a minimal, production‑ready snippet that requests the camera permission from a

code
Fragment
. The same pattern works in an
code
Activity
by replacing
code
this
with
code
registerForActivityResult
.

kotlin
// In MyFragment.kt
class CameraPermissionFragment : Fragment() {
    // 1. Create a launcher using the GetPermission contract
    private val cameraLauncher = registerForActivityResult(
        ActivityResultContracts.GetPermission()
    ) { isGranted ->
        // 2. Handle the result
        if (isGranted) {
            openCamera()
        } else {
            showPermissionRationale()
        }
    }

    // 3. Helper to request permission when needed
    private fun requestCameraPermission() {
        cameraLauncher.launch(android.Manifest.permission.CAMERA)
    }

    // Example lifecycle callback – request on first view
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        requestCameraPermission()
    }

    // UI actions that need the permission
    private fun openCamera() {
        // Launch camera intent or navigate to camera screen
    }

    private fun showPermissionRationale() {
        // Show a short explanation and optionally direct to settings
        Snackbar.make(requireView(), "Camera permission is required to capture photos.", Snackbar.LENGTH_LONG).show()
    }
}

[!TIP] Use

code
ActivityResultContracts.GetPermission()
instead of the older
code
requestPermissions
. It returns a Boolean directly, removing the need to parse the permission array in
code
onRequestPermissionsResult
.

Handling Rationale and Edge Cases

The Activity Result API does not automatically show the system’s “Reason for needing this permission” dialog. You must check

code
shouldShowRequestPermissionRationale
before launching the permission request. This ensures you give users a clear explanation when they previously denied the permission without checking “Never ask again”.

kotlin
private fun requestCameraPermission() {
    if (shouldShowRequestPermissionRationale(android.Manifest.permission.CAMERA)) {
        // Show custom rationale dialog
        PermissionUtils.showRationaleDialog(
            requireContext(),
            "Camera access lets you take photos directly in the app.",
            { /* User accepted the explanation */ }
        ) {
            cameraLauncher.launch(android.Manifest.permission.CAMERA)
        }
    } else {
        cameraLauncher.launch(android.Manifest.permission.CAMERA)
    }
}

[!WARNING] Skipping the rationale check can lead to a poor user experience. Users may deny a permission without understanding why, increasing uninstall rates.

Backward Compatibility with Older APIs

code
ActivityResultContracts.GetPermission
was added in API level 33 (Android 13). If you support older devices, you need a fallback. The simplest approach is to keep the legacy
code
requestPermissions
for devices below API 33 while using the new contract for newer devices.

kotlin
private fun requestCameraPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        // Use Activity Result API on Tiramisu+ (API 33+)
        cameraLauncher.launch(android.Manifest.permission.CAMERA)
    } else {
        // Legacy path for older devices
        requestPermissions(arrayOf(android.Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
    }
}

[!IMPORTANT] Always guard the new API with a version check. Mixing both approaches keeps your app functional on older devices while enjoying the cleaner flow on newer ones.

Best Practices and Real‑World Patterns

1. Centralize Permission Logic

In a solo‑dev shop like SudarshanTechLabs, you often have multiple apps sharing similar permission needs. Create a small utility class (

code
PermissionHelper.kt
) that exposes a single
code
requestPermission
function. This reduces duplication and makes testing easier.

kotlin
// PermissionHelper.kt
object PermissionHelper {
    fun requestPermission(
        host: Fragment,
        permission: String,
        launcher: MutableState<ActivityResultLauncher<String>> // optional, can be created inside)
    ) {
        val launch = launcher ?: host.registerForActivityResult(
            ActivityResultContracts.GetPermission()
        ) { /* result handled by caller */ }
        if (host.shouldShowRequestPermissionRationale(permission)) {
            // Show rationale
        }
        launch.launch(permission)
    }
}

2. Use Coroutines for UI Updates

When you need to perform network work after a permission is granted, wrap the permission request in a coroutine scope. This keeps the UI responsive and avoids leaking lifecycles.

kotlin
private fun requestCameraAndFetch() = lifecycleScope.launch {
    cameraLauncher.launch(android.Manifest.permission.CAMERA)
    // Wait for result – you need a StateFlow or a channel to bridge the callback
    // Example using a simple flag:
    permissionGranted.collectLatest { granted ->
        if (granted) {
            val photo = fetchPhotoFromNetwork()
            // Update UI
            _uiState.postValue(UiState.Success(photo))
        }
    }
}

3. Persist User Preference for “Never Ask Again”

If a user selects “Never ask again”, you should respect that choice. Store a boolean in

code
SharedPreferences
and skip future permission requests unless the user revisits settings and explicitly re‑enables the permission.

kotlin
private fun shouldRequestPermission(permission: String): Boolean {
    val neverAsk = context?.getSharedPreferences("perm_prefs", Context.MODE_PRIVATE)
        ?.getBoolean("${permission}_never_ask", false) ?: false
    return !neverAsk && ContextCompat.checkSelfPermission(context!!, permission) != PackageManager.PERMISSION_GRANTED
}

4. Test Your Permission Flow

Unit tests for permission logic are straightforward with

code
ActivityScenario
. You can simulate a permission grant by calling
code
scenario.onActivity { activity -> ... }
and using
code
InstrumentationRegistry.getTargetContext()
to invoke the launcher.

kotlin
@RunWith(AndroidJUnit4::class)
class PermissionHelperTest {
    @get:Rule
    val scenario = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun `grant permission updates UI`() {
        // Mock the launcher to return true
        // Verify UI state changes
    }
}

[!TIP] Write a small integration test that actually triggers the permission request and verifies the UI updates. This catches regressions early, especially when you refactor to the Activity Result API.

Key Takeaways

  • Adopt the Activity Result API for all new permission requests; it provides lifecycle‑safe launchers and cleaner callbacks compared to
    code
    requestPermissions
    .
  • Check
    code
    shouldShowRequestPermissionRationale
    before launching to give users a meaningful explanation and improve consent rates.
  • Guard the new API with a version check to maintain compatibility with devices below API 33, and keep a fallback path for older builds.
  • Centralize permission logic in a helper class or repository to reduce duplication across multiple apps and simplify testing.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this