Structured Concurrency in Android

“Concurrency without structure is chaos. Structured concurrency makes coroutines predictable, safe, and lifecycle-aware.”

As Android engineers, we juggle multiple tasks: fetching API data, updating UI, caching results, syncing offline data, and handling user interactions—all happening asynchronously. Without proper management, this can lead to leaks, zombie coroutines, or even crashes.

This is where Structured Concurrency in Kotlin comes in. It enforces scoped, hierarchical coroutine management that aligns perfectly with Android lifecycles.


 What is Structured Concurrency?

Structured concurrency means that every coroutine has a parent scope, and when the parent is canceled (e.g., Activity destroyed, ViewModel cleared), all its child coroutines are automatically canceled.

Instead of launching fire-and-forget coroutines (like GlobalScope.launch), structured concurrency ensures:

  • Lifecycle awareness: Coroutines cancel when their UI component dies.

  • Error propagation: Exceptions bubble up to parent scopes.

  • Predictable flow: All children complete or cancel together.


 Building Blocks in Android

1. CoroutineScope

Defines the lifecycle boundary for coroutines.

  • viewModelScope → Tied to ViewModel lifecycle.

  • lifecycleScope → Tied to Activity/Fragment lifecycle.

  • rememberCoroutineScope → For Composables.

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = repository.getData()
            _uiState.value = data
        }
    }
}

2. Job & SupervisorJob

  • Job: Cancels all child coroutines when canceled.

  • SupervisorJob: Allows siblings to fail independently.

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

3. Coroutine Builders

  • launch { } → Fire-and-forget.

  • async { } → Returns Deferred<T> result, use await().

  • withContext { } → Switch dispatcher safely.

  • coroutineScope { } → Ensures children complete or cancel as a group.


 Real-World Use Cases in Android

1. Safe API Calls in ViewModel

class UserViewModel(private val repo: UserRepository) : ViewModel() {
    val userState = MutableStateFlow<User?>(null)

    fun loadUser() {
        viewModelScope.launch {
            try {
                val user = repo.fetchUser()
                userState.value = user
            } catch (e: IOException) {
                // Handle error gracefully
            }
        }
    }
}

 When the ViewModel clears, viewModelScope cancels the coroutine, preventing memory leaks.


2. Parallel Requests with async

suspend fun fetchProfileAndPosts() = coroutineScope {
    val profile = async { api.getProfile() }
    val posts = async { api.getPosts() }
    profile.await() to posts.await()
}

 If one request fails, both are canceled, avoiding inconsistent UI states.


3. With Timeout

withTimeout(5000) {
    repository.syncData()
}

 Cancels automatically if it takes longer than 5 seconds.


 4. In Jetpack Compose

@Composable
fun UserScreen(repo: UserRepository) {
    val scope = rememberCoroutineScope()
    var name by remember { mutableStateOf("") }

    Button(onClick = {
        scope.launch {
            name = repo.getUser().name
        }
    }) {
        Text("Load User")
    }
}

When the Composable leaves the composition, rememberCoroutineScope is canceled.


 Error Handling in Structured Concurrency

Centralized Exception Handling

val handler = CoroutineExceptionHandler { _, exception ->
    Log.e("CoroutineError", "Caught: $exception")
}

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + handler)

Supervisor Scope Example

suspend fun safeParallelCalls() = supervisorScope {
    val first = launch { riskyCall1() }
    val second = launch { riskyCall2() }
    // If one fails, the other keeps running
}

 Best Practices for Android Engineers

  1. Never use GlobalScope → Causes leaks and unmanaged coroutines.

  2. Always tie to lifecycle → Use viewModelScope, lifecycleScope, or rememberCoroutineScope.

  3. Use coroutineScope or supervisorScope for child tasks.

  4. Use Dispatchers wisely:

    • Main → UI updates.

    • IO → Network/database.

    • Default → CPU-intensive tasks.

  5. Write tests with runTest or TestCoroutineDispatcher.

  6. Handle cancellation properly (isActive, ensureActive()).


Analogy: Family Trip 


Think of structured concurrency like a family trip:
  • The parent (scope) starts the trip.

  • All kids (child coroutines) must stick together.

  • If the parent says "Trip’s over," everyone goes home.

  • No child keeps wandering alone (no leaks).


Closing Thoughts

Structured concurrency is not just a Kotlin feature—it’s a mindset. It brings discipline to concurrency, making apps safer, cleaner, and more maintainable.

As Android engineers, embracing structured concurrency ensures our coroutines don’t outlive their purpose, keeping apps fast, stable, and memory-leak free.


- By following these practices, you’ll write modern, production-ready Android code with Kotlin coroutines that scales across features, modules, and teams.


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป




Token Management in Android App Development: Best Practices for Engineers

As Android engineers, we’re constantly building apps that need to securely authenticate users and interact with backend APIs. One of the most critical parts of this puzzle is token management — making sure authentication is safe, seamless, and user-friendly.


 What is a Token?

A token is a digital key that grants access to protected resources (like APIs). Instead of repeatedly sending usernames and passwords (insecure and inefficient), the client app uses tokens to prove identity and permissions.

Tokens usually come in two flavors:

  • Access Token – short-lived, used for every API request.

  • Refresh Token – longer-lived, used to obtain a new access token once it expires.

Think of an access token as a hotel room key card and the refresh token as your hotel booking record. If your key card stops working, the receptionist (auth server) issues a new one, as long as your booking is valid.


Token Lifecycle in Android

Here’s the typical flow:

  1. Login – User authenticates with email/password/biometric → server issues access + refresh token.

  2. Store Securely – Tokens are stored securely on-device.

  3. Use in Requests – Access token is attached to each API call.

  4. Refresh When Expired – On 401 Unauthorized, refresh token is used to fetch a new access token.

  5. Logout – Tokens are cleared.

 Visual Flow:


 Best Practices for Token Management

  1. Use Access + Refresh Token Strategy

    • Access tokens: short-lived.

    • Refresh tokens: long-lived but securely stored.

  2. Secure Storage

    • Use EncryptedSharedPreferences / Encrypted DataStore.

    • Use Android Keystore for refresh tokens (hardware-backed security).

    • Keep access token in memory only to reduce exposure.

  3. Automatic Token Handling

    • Use OkHttp Interceptor to attach tokens.

    • Use OkHttp Authenticator to refresh tokens when expired.

    • Never manually add tokens in API calls.

  4. Follow Clean Architecture

    • Keep token logic in Data layer.

    • Expose login/refresh/logout via Use Cases (Domain layer).

    • UI observes AuthState from ViewModel.

  5. Security Best Practices

    • Always use HTTPS (TLS).

    • Never log tokens.

    • Force logout if refresh fails.

    • Test token expiration scenarios.


 Example Implementation in Android

Retrofit API

interface AuthApi {
    @POST("auth/login")
    suspend fun login(@Body request: LoginRequest): TokenResponse

    @POST("auth/refresh")
    suspend fun refreshToken(@Body request: RefreshRequest): TokenResponse
}

Token Storage

class TokenStorage(private val context: Context) {
    private val Context.dataStore by preferencesDataStore("secure_prefs")
    private val ACCESS_TOKEN = stringPreferencesKey("access_token")
    private val REFRESH_TOKEN = stringPreferencesKey("refresh_token")

    suspend fun saveTokens(access: String, refresh: String) {
        context.dataStore.edit {
            it[ACCESS_TOKEN] = access
            it[REFRESH_TOKEN] = refresh
        }
    }

    suspend fun getAccessToken() = context.dataStore.data.first()[ACCESS_TOKEN]
    suspend fun getRefreshToken() = context.dataStore.data.first()[REFRESH_TOKEN]
    suspend fun clearTokens() = context.dataStore.edit { it.clear() }
}

OkHttp Interceptor

class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider.getAccessToken()
        val newRequest = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(newRequest)
    }
}

OkHttp Authenticator (Auto Refresh)

class TokenAuthenticator(private val repo: AuthRepository) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        return runBlocking {
            if (repo.refreshToken()) {
                repo.getAccessToken()?.let { newToken ->
                    response.request.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                }
            } else null
        }
    }
}

Key Takeaways

  • Access tokens should be short-lived → reduces risk.

  • Refresh tokens should be secured in Keystore → prevents theft.

  • Automatic handling via Interceptor + Authenticator → reduces bugs.

  • Clean architecture separation → makes token logic testable + maintainable.

By following these best practices, you’ll deliver apps that are secure, reliable, and seamless for users.


Common Mistakes to Avoid

Even experienced developers sometimes fall into these pitfalls:

  1. Storing tokens in plain SharedPreferences

    • ❌ Vulnerable to root access and backup extraction.

    • ✅ Use Encrypted DataStore or Keystore.

  2. Using long-lived access tokens

    • ❌ If stolen, attackers can access APIs indefinitely.

    • ✅ Keep access tokens short-lived, rely on refresh tokens.

  3. Not handling 401 Unauthorized properly

    • ❌ App just crashes or shows an error.

    • ✅ Automatically refresh the token using Authenticator.

  4. Storing refresh tokens in memory only

    • ❌ Refresh token will be lost when the app restarts, forcing re-login.

    • ✅ Store refresh token securely on disk (Keystore-backed).

  5. Logging tokens in Crashlytics or Logcat

    • ❌ Attackers can retrieve sensitive tokens from logs.

    • ✅ Never log tokens, redact them if necessary.

  6. Not clearing tokens on logout

    • ❌ User data can remain exposed.

    • ✅ Always clear tokens from secure storage on logout.

  7. Refreshing tokens too eagerly

    • ❌ Wastes server resources and battery.

    • ✅ Refresh only when expired (lazy refresh).


 Token Lifecycle Management

  • On login → store both access + refresh token securely.

  • On every request → add access token.

  • On 401 → refresh token automatically.

  • On app start → check if refresh token exists & is valid.

  • On logout → clear all tokens from storage.




Final Words

Token management might sound like a low-level detail, but in reality, it’s the backbone of secure Android apps. Whether you’re building fintech, healthcare, or large-scale consumer apps, these practices ensure your app is both safe and user-friendly.


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป