Runtime permissions, background threads and network calls
Ask for permissions the right way at the right moment, and keep slow work off the main thread with coroutines.
Two things crash or get an app rejected faster than anything else: touching the camera or location without permission, and doing network work on the main thread. Both have a standard pattern, and both are reliable interview topics.
Normal versus dangerous permissions
Every permission goes in the manifest. Normal ones such as INTERNET are granted at install. Dangerous ones such as CAMERA, ACCESS_FINE_LOCATION and POST_NOTIFICATIONS must be requested while the app runs, at the moment the feature is used, with a short reason if the user has refused before.
<uses-permission android:name="android.permission.CAMERA" />Requesting with the Activity Result API
Register the launcher as a property, so it exists before the activity is created, then launch it from the click handler.
private val requestCamera =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) openCamera() else showCameraUnavailableMessage()
}
private fun onTakePhotoClicked() {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED -> openCamera()
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) ->
showRationaleDialog { requestCamera.launch(Manifest.permission.CAMERA) }
else -> requestCamera.launch(Manifest.permission.CAMERA)
}
}Three outcomes are possible: already granted, granted now, or denied. If the user denies twice, the system stops showing the dialog and granted comes back false immediately, so your fallback message should explain how to enable it in settings. Never block the whole app behind a permission that only one feature needs.
Keeping the main thread free
The main thread draws the screen and handles touches. A network call on it throws NetworkOnMainThreadException; a long database read freezes the UI. Coroutines in a ViewModel are the standard fix. viewModelScope cancels the work when the ViewModel is cleared, and withContext(Dispatchers.IO) moves the slow part to a background pool.
class JobViewModel(private val repo: JobRepository) : ViewModel() {
private val _jobs = MutableLiveData<List<Job>>()
val jobs: LiveData<List<Job>> = _jobs
fun load() {
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) { repo.fetchJobs() }
_jobs.value = result
} catch (e: IOException) {
_jobs.value = emptyList()
}
}
}
}Choose the dispatcher by the kind of work: Dispatchers.IO for network and disk, Dispatchers.Default for heavy calculation, Dispatchers.Main for touching views. Because launch above starts on Main, setting _jobs.value after withContext returns is safe. Work that must survive the app being closed, such as uploading a queued form, belongs in WorkManager rather than a coroutine.
Common mistakes and a practice habit
- Requesting every permission on the splash screen. Users refuse what they do not yet understand.
- Calling
registerForActivityResultinside a click listener, which fails because registration must happen before the activity starts. - Using
GlobalScope, which never cancels and leaks work after the screen closes. - Updating a view from
Dispatchers.IO, which crashes with a wrong-thread error.
Practice habit: add a photo button to a sample app, walk through grant, deny and deny-twice on a real device, then fetch a JSON list in a ViewModel and confirm the list scrolls smoothly while loading.
More in Web and app