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.
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.
On this page
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
requestPermissions()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
PermissionManager.requestPermissions()| Feature | Legacy code | Activity Result API ( code |
|---|---|---|
| Lifecycle safety | Requires manual handling of config changes | Automatically re‑registered after process restart |
| Result handling | code | code code |
| Testing | Needs mock code code | Can mock code |
| UX consistency | Varies per API level | Uniform across API levels, works with code |
| Backward compatibility | Works on all supported APIs | Requires API 33+ for code |
[!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.
The Activity Result API was introduced as part of AndroidX Activity library and later expanded to include permission contracts. The key is to pair
registerForActivityResultActivityResultContractGetPermissionBelow is a minimal, production‑ready snippet that requests the camera permission from a
FragmentActivitythisregisterForActivityResult// 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
instead of the oldercodeActivityResultContracts.GetPermission(). It returns a Boolean directly, removing the need to parse the permission array incoderequestPermissions.codeonRequestPermissionsResult
The Activity Result API does not automatically show the system’s “Reason for needing this permission” dialog. You must check
shouldShowRequestPermissionRationaleprivate 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.
ActivityResultContracts.GetPermissionrequestPermissionsprivate 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.
In a solo‑dev shop like SudarshanTechLabs, you often have multiple apps sharing similar permission needs. Create a small utility class (
PermissionHelper.ktrequestPermission// 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)
}
}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.
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))
}
}
}If a user selects “Never ask again”, you should respect that choice. Store a boolean in
SharedPreferencesprivate 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
}Unit tests for permission logic are straightforward with
ActivityScenarioscenario.onActivity { activity -> ... }InstrumentationRegistry.getTargetContext()@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.
requestPermissionsshouldShowRequestPermissionRationaleSudarshan 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.
Related Posts
Related Apps
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 meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read