Android SharedPreferences Tutorial and Example

Using Coroutines For Asynchronous Android Apps

Modern Android applications constantly perform work away from the main thread. They fetch data from an API, read a SQLite database, resize images, process files and save user settings. If this work runs on the UI thread, screens can freeze and Android may display an Application Not Responding warning.

Kotlin coroutines provide a readable way to manage these operations. They allow a function to pause while waiting for a result, then resume without blocking the thread. This approach is easier to follow than deeply nested callbacks and works well with Android components such as ViewModel, Room, Retrofit and WorkManager.

For Australian users, reliable background processing matters across different conditions. Someone checking a transport app on a Sydney train may move between strong and weak mobile coverage, while a regional user near Toowoomba may depend on a slower connection. Coroutines help the app remain responsive during both situations.

This guide explains the core coroutine concepts, shows where dispatchers belong, and demonstrates patterns suitable for beginner Android projects. It also covers cancellation, error handling, lifecycle awareness and the careful storage of small preferences.

Understanding Suspend Functions And Dispatchers

A coroutine is a lightweight unit of asynchronous work. It can suspend without blocking its current thread and later continue from the same point. A function marked with suspend may call other suspending functions, such as a network request or a database query, but it cannot be called directly from an ordinary function without a coroutine scope.

Dispatchers determine where coroutine work runs. Dispatchers.Main is intended for UI updates, Dispatchers.IO suits network and file operations, and Dispatchers.Default is designed for CPU-heavy work such as sorting a large collection or decoding data. Switching context is explicit:

suspend fun loadProducts(): List<Product> {
    return withContext(Dispatchers.IO) {
        repository.fetchProducts()
    }
}

The withContext block suspends the function while the operation runs on an appropriate worker thread. Once it finishes, execution returns to the original context. This keeps the interface responsive without requiring manual thread management.

Launching Work With Lifecycle Awareness

A coroutine should belong to a scope whose lifetime matches the work. In an activity or fragment, lifecycleScope cancels its jobs when the lifecycle is destroyed. In a ViewModel, viewModelScope keeps work alive through configuration changes such as screen rotation and cancels it when the ViewModel is cleared.

A common ViewModel pattern exposes loading, success and failure states:

class ProductsViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    val products = MutableStateFlow<List<Product>>(emptyList())
    val error = MutableStateFlow<String?>(null)

    fun refresh() {
        viewModelScope.launch {
            try {
                products.value = repository.loadProducts()
            } catch (exception: IOException) {
                error.value = "Unable to load products"
            }
        }
    }
}

For a Compose interface, collectAsStateWithLifecycle() can observe these flows safely. For a traditional XML layout, a fragment can collect the state inside viewLifecycleOwner.lifecycleScope. This distinction is important because a fragment’s view can be destroyed before the fragment itself, especially during navigation.

Choosing Between Common Asynchronous Patterns

Coroutines work alongside several Android technologies rather than replacing every one of them. A suspend function is useful for one result, while Flow is better for values that change over time. async and await can run independent tasks concurrently, although sequential code is usually clearer when operations depend on each other.

Android task Suitable coroutine approach Typical dispatcher Cancellation behaviour
Load one API response suspend function Dispatchers.IO Stops when the parent scope is cancelled
Observe database changes Flow collection Room-managed or IO context Collection ends with the lifecycle
Run independent requests async with structured concurrency IO Child jobs share parent cancellation
Resize or analyse images withContext Dispatchers.Default Stops if the operation cooperates
Guaranteed background work WorkManager with a coroutine worker Worker-managed Can continue after the screen closes

For example, a product screen could request details and reviews in parallel with coroutineScope and two async calls. Structured concurrency ensures that child jobs remain attached to their parent. If one request fails or the screen disappears, the related work can be cancelled instead of continuing invisibly.

Small values such as a selected theme or a first-launch flag can remain available between sessions through SharedPreferences storage. The preference read itself is simple, but larger or sensitive data should use a suitable database or encrypted storage strategy.

Handling Errors Cancellation And Repeated Requests

A try and catch block around a coroutine gives the interface a predictable response to failures. Network code should distinguish an unavailable connection, an HTTP error and malformed data where practical. Avoid catching every Throwable, because cancellation uses CancellationException and should normally be allowed to propagate.

Search boxes are a common place for cancellation. When a user types “coffee” in a Melbourne café finder, then quickly changes it to “coffee near Richmond”, the previous request should not overwrite the newer result. A StateFlow combined with debounce, distinctUntilChanged and flatMapLatest can cancel stale searches as new input arrives.

val results: StateFlow<List<Cafe>> =
    query
        .debounce(300)
        .filter { it.length >= 2 }
        .distinctUntilChanged()
        .flatMapLatest { text -> repository.search(text) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

Cancellation is cooperative. Suspending functions such as Retrofit calls and delay usually respond automatically, while a long CPU loop must check isActive or call yield(). This prevents abandoned work from consuming battery and processing power on devices used throughout a long commute.

Building A Practical Coroutine-Based Screen

A dependable screen commonly follows this sequence: the ViewModel launches work, the repository selects the correct data source, and the UI observes state. The repository can call a remote service first, save successful results to Room, and then expose the cached records through a Flow. This supports users in Perth or regional Queensland when connectivity is intermittent.

Use Dispatchers.IO inside the repository or data layer rather than scattering dispatcher choices through activities and fragments. Keep UI code focused on rendering. A sealed state model can make the contract clear:

sealed interface ScreenState {
    data object Loading : ScreenState
    data class Ready(val items: List<Product>) : ScreenState
    data class Failed(val message: String) : ScreenState
}

Testing becomes easier when dispatchers are injected instead of hard-coded everywhere. A test dispatcher can execute virtual time and predictable jobs, allowing checks for loading states, retries and cancellation. Android Studio’s coroutine debugging tools can also reveal jobs that remain active after a screen has gone away.

Australian apps should consider privacy and data minimisation from the beginning. The Privacy Act 1988 and the Australian Privacy Principles can affect products that collect personal information, particularly when analytics, location or account details are involved. Do not place tokens, passwords or private customer data in ordinary preferences, and explain data use in clear language.

Recommendations For Reliable Coroutine Code

Good coroutine design is mostly about boundaries and ownership. Each job should have a clear parent, each blocking operation should use an appropriate dispatcher, and every visible state should have a defined success and failure path. These practices are especially valuable for apps that must work across Australia’s varied networks and device conditions.

  • Use viewModelScope for screen-related business operations and lifecycleScope for short-lived UI actions.
  • Keep network, database and file work away from Dispatchers.Main.
  • Prefer Flow for changing data and suspend functions for one-time results.
  • Propagate cancellation instead of catching and hiding CancellationException.
  • Store only suitable non-sensitive preferences locally, following privacy obligations for collected information.

Coroutines make asynchronous Android programming read much like ordinary Kotlin code, while still supporting cancellation, concurrency and lifecycle safety. With structured scopes and clear data-layer responsibilities, an app can stay responsive whether it is loading a list over a busy Brisbane connection or displaying cached data offline.