HomebaseWorks
Lesson 4 of 4

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.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

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.

Advertisement

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 registerForActivityResult inside 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.

Advertisement
Continue with Android4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Web and app