Popular Searches
Popular Course Categories
Popular Courses

Solving Coroutines, API Integration, and Database Interview Problems

Solving Coroutines, API Integration, and Database Interview Problems

Android App Development Portfolio and Interview Preparation

Solving Coroutines, API Integration, and Database Interview Problems

Course: Android App Development

Topic: Solving Coroutines, API Integration, and Database Interview Problems

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

Institute Website: JustAcademy Official Website


1. Introduction

Coroutines, API integration, and database management are important topics in Android developer interviews. Interviewers often evaluate whether a candidate can perform asynchronous operations, retrieve data from a remote server, store data locally, manage application state, and handle errors without making the interface unresponsive.

Modern Android applications commonly use Kotlin coroutines for asynchronous programming, Retrofit for REST API communication, Room for local persistence, Flow for observing database changes, and ViewModel for managing screen-related state.

These technologies work together in practical applications such as shopping apps, banking apps, student management systems, expense trackers, chat applications, and news applications.

2. Learning Objectives

  • Understand coroutine concepts and solve common coroutine interview questions.
  • Differentiate between suspend functions, launch, async, and withContext.
  • Understand coroutine scopes, dispatchers, cancellation, and exception handling.
  • Integrate REST APIs using Retrofit and handle HTTP responses.
  • Use Room Database for local storage and database queries.
  • Understand Flow and StateFlow for observing changing data.
  • Combine API responses with local database operations through a repository.
  • Solve practical problems involving concurrency, caching, synchronization, and failures.
  • Prepare clear technical explanations and code-based interview answers.

3. Understanding Kotlin Coroutines

A coroutine is a suspendable computation that can pause and resume without blocking the underlying thread while waiting for an operation to complete. Coroutines make asynchronous code easier to read and maintain.

For example, when an Android application requests user information from a server, the request may take several milliseconds or seconds. A coroutine allows the application to wait for the result without blocking the main UI thread.

3.1 Traditional Blocking Approach

// Conceptual example only: blocking network work
// should not run on Android's main thread.
val result = performBlockingRequest()
displayResult(result)

A blocking operation occupies its thread while waiting. If the main thread is blocked for too long, scrolling, animations, and touch interactions may become unresponsive.

3.2 Coroutine-Based Approach

viewModelScope.launch {
    val result = repository.getUser()
    // Update UI state with the result.
}

The coroutine can suspend while waiting for the result, allowing the thread to perform other work. The result can then be used to update the UI state.

4. Important Coroutine Concepts

Concept Meaning Common Use
suspend Marks a function that can suspend its coroutine. Network and database operations.
launch Starts a coroutine and returns a Job. Performing work without returning a direct result.
async Starts a coroutine and returns a Deferred result. Concurrent operations that produce values.
await Suspends until a Deferred result is available. Retrieving an async operation's result.
withContext Runs a suspending block in a specified coroutine context. Moving blocking work to an appropriate dispatcher.
CoroutineScope Defines the lifecycle and context for coroutines. Managing related asynchronous tasks.
Job Represents a coroutine's lifecycle. Cancellation and completion tracking.
Flow Represents an asynchronous stream of values. Observing changing database data.
StateFlow Represents observable state with a current value. Exposing UI state from a ViewModel.

5. Interview Problem: Difference Between launch and async

Question

What is the difference between launch and async in Kotlin coroutines?

Answer

launch is used when a coroutine performs an operation without needing to return a value directly. It returns a Job. async is used when a coroutine produces a value that can be retrieved using await(); it returns a Deferred.

import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

suspend fun demonstrateCoroutines() = coroutineScope {
    launch {
        println("Saving user activity")
    }

    val total = async {
        10 + 20
    }

    println("Total: ${total.await()}")
}

In this example, launch starts an operation that does not produce a result for the caller, while async computes a value.

Interview Tip

Do not use async simply because a task is asynchronous. Use it when a result is required. Use launch for work whose result is represented through another mechanism, such as state updates or a database write.

6. Interview Problem: What Is a suspend Function?

Question

What does the suspend keyword mean in Kotlin?

Answer

The suspend keyword allows a function to suspend the current coroutine and resume later. A suspending function can be called from another suspending function or a coroutine builder.

suspend fun calculateTotal(): Int {
    return 100 + 50
}

viewModelScope.launch {
    val total = calculateTotal()
    println(total)
}

A suspending function does not automatically run on a background thread. It may execute on the current coroutine context unless the implementation or called library changes the execution context. Therefore, blocking work must be moved to an appropriate dispatcher.

7. Interview Problem: Understanding Coroutine Dispatchers

A dispatcher determines where coroutine work is scheduled. Choosing an appropriate dispatcher helps keep the Android main thread responsive.

Dispatcher Typical Use
Dispatchers.Main UI-related operations and state updates.
Dispatchers.IO Blocking I/O operations such as file or legacy database work.
Dispatchers.Default CPU-intensive work such as large computations or processing.
Dispatchers.Unconfined Specialized cases; generally not the default choice for application work.

Example: Moving CPU-Intensive Work

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

suspend fun calculateLargeResult(): Long =
    withContext(Dispatchers.Default) {
        (1L..1_000_000L).sum()
    }

Use Dispatchers.Default for CPU-heavy calculations. Use Dispatchers.IO for blocking I/O that does not already provide a main-safe suspending API.

Important Interview Point

Retrofit's suspend API functions and Room's suspend DAO functions are designed to be called from a coroutine without manually switching to Dispatchers.IO for every call. However, if you write your own suspending function that performs blocking work, you should make that function main-safe by moving the blocking operation to an appropriate dispatcher.

8. Interview Problem: Sequential vs. Parallel API Requests

Question

How can two independent API requests be executed concurrently?

Suppose an application needs to fetch a user's profile and notification count. If the requests are independent, they can run concurrently instead of waiting for one to finish before starting the other.

Sequential Approach

suspend fun loadDashboard(): Dashboard {
    val profile = api.getProfile()
    val notifications = api.getNotificationCount()

    return Dashboard(
        profile = profile,
        notifications = notifications
    )
}

Concurrent Approach

import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

suspend fun loadDashboard(): Dashboard =
    coroutineScope {
        val profileTask = async {
            api.getProfile()
        }

        val notificationTask = async {
            api.getNotificationCount()
        }

        Dashboard(
            profile = profileTask.await(),
            notifications = notificationTask.await()
        )
    }

The concurrent approach is useful when both requests are independent. If one request depends on the result of the other, they must be sequenced or coordinated appropriately.

Interview Tip

Structured concurrency keeps child coroutines associated with their parent scope. When a child fails, its failure can affect sibling coroutines depending on the scope and exception-handling strategy. Use supervision only when independent failure handling is intentional.

9. Interview Problem: Coroutine Cancellation

Question

Why is coroutine cancellation important in Android?

Cancellation prevents unnecessary work when the result is no longer needed. For example, when a user leaves a screen, screen-specific requests should generally stop if their lifecycle no longer requires them.

class ProfileViewModel(
    private val repository: ProfileRepository
) : ViewModel() {

    fun loadProfile() {
        viewModelScope.launch {
            val profile = repository.getProfile()
            // Publish profile to UI state.
        }
    }
}

Coroutines launched in viewModelScope are cancelled when the ViewModel is cleared. For collection tied to a visible screen, use lifecycle-aware collection such as repeatOnLifecycle with collect.

Cancellation Best Practices

  • Avoid using unmanaged global scopes for screen-specific tasks.
  • Do not swallow CancellationException in broad exception handlers.
  • Use cancellable APIs and check cancellation during long-running custom loops.
  • Cancel obsolete search requests when a newer query is entered.
  • Use lifecycle-aware collection for UI state streams.

10. Interview Problem: Debouncing Search Requests

When a user types into a search field, sending an API request after every keystroke can waste bandwidth and overload the server. Debouncing waits until typing has paused for a short period before issuing a search request.

import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map

@OptIn(FlowPreview::class)
fun searchResults(
    queries: Flow<String>,
    repository: SearchRepository
): Flow<List<SearchItem>> {
    return queries
        .map { it.trim() }
        .debounce(300)
        .distinctUntilChanged()
        .flatMapLatest { query ->
            repository.search(query)
        }
}

flatMapLatest switches to the newest search flow and cancels collection of the previous one. This is useful for search interfaces where older results are no longer relevant. Ensure the repository's implementation cooperates with cancellation.

11. Introduction to API Integration

An API, or Application Programming Interface, allows an Android application to communicate with another system. A REST API typically uses HTTP methods and JSON request or response bodies.

Common HTTP Methods

Method Purpose Example
GET Retrieve information. Fetch a list of products.
POST Create a resource or perform an action. Submit a registration form.
PUT Replace a resource. Replace a user's editable profile.
PATCH Partially update a resource. Update a display name.
DELETE Delete a resource. Delete a saved item.

12. Interview Problem: Integrating Retrofit

Retrofit converts annotated API interfaces into HTTP clients. It is commonly used with OkHttp for networking and a converter such as Gson or Kotlin serialization for JSON parsing.

12.1 Define a Data Model

data class UserDto(
    val id: Int,
    val name: String,
    val email: String
)

The properties must match the API response or use the converter's supported annotations to map differently named fields.

12.2 Define the API Interface

import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface UserApi {
    @GET("users")
    suspend fun getUsers(): Response<List<UserDto>>

    @GET("users/{id}")
    suspend fun getUser(
        @Path("id") id: Int
    ): Response<UserDto>
}

12.3 Configure Retrofit

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

fun createRetrofit(): Retrofit {
    return Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .addConverterFactory(
            GsonConverterFactory.create()
        )
        .build()
}

The base URL above is a placeholder. Replace it with the real HTTPS endpoint and configure compatible library dependencies in the project.

13. Interview Problem: Handling API Success and Failure

A reliable API implementation should distinguish successful responses, unsuccessful HTTP responses, network errors, parsing failures, and cancellation.

suspend fun fetchUsers(): Result<List<UserDto>> {
    return try {
        val response = userApi.getUsers()

        if (response.isSuccessful) {
            val users = response.body()

            if (users != null) {
                Result.success(users)
            } else {
                Result.failure(
                    IllegalStateException(
                        "Empty response body"
                    )
                )
            }
        } else {
            Result.failure(
                IllegalStateException(
                    "HTTP ${response.code()}"
                )
            )
        }
    } catch (cancelled: CancellationException) {
        throw cancelled
    } catch (exception: Exception) {
        Result.failure(exception)
    }
}

This example uses Result to demonstrate error reporting. A production app may prefer a sealed result type that distinguishes network errors, authentication failures, validation errors, server errors, and unexpected responses.

Common HTTP Status Codes

Status Meaning Typical Handling
200 OK Process the successful response.
201 Created Process the newly created resource.
204 No Content Handle success without a response body.
400 Bad Request Inspect validation and request-format errors.
401 Unauthorized Check credentials or session expiration.
403 Forbidden Handle insufficient permissions.
404 Not Found Handle missing resources or endpoints.
429 Too Many Requests Respect rate limits and retry guidance.
500–599 Server Errors Show a suitable error and retry when safe.

14. Interview Problem: What Is an Interceptor?

An OkHttp interceptor can inspect or modify outgoing requests and incoming responses. A common use is attaching an authorization header to requests that require authentication.

class AuthInterceptor(
    private val tokenProvider: () -> String?
) : Interceptor {

    override fun intercept(
        chain: Interceptor.Chain
    ): Response {
        val original = chain.request()
        val token = tokenProvider()

        val request = if (token != null) {
            original.newBuilder()
                .header(
                    "Authorization",
                    "Bearer $token"
                )
                .build()
        } else {
            original
        }

        return chain.proceed(request)
    }
}

Configure this interceptor on an OkHttpClient and use that client with Retrofit. Ensure that credentials are not sent to untrusted hosts. Never log access tokens, passwords, or other sensitive information.

15. Introduction to Android Database Management

Local persistence allows an application to save data on the device. Android applications can use Room, a library that provides an abstraction over SQLite with compile-time query validation and structured database access.

Room is useful for caching API responses, storing notes, maintaining product information, saving offline content, and supporting data-driven screens.

15.1 Main Room Components

Component Purpose
Entity Defines a database table.
DAO Defines database queries and write operations.
RoomDatabase Provides access to DAOs and database configuration.
Migration Defines how the schema changes between versions.
Type converter Converts supported representations for fields requiring custom storage.

16. Interview Problem: Create a Room Entity

Suppose an application stores products locally. Each product needs a stable identifier, title, and price.

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "products")
data class ProductEntity(
    @PrimaryKey
    val id: Int,
    val title: String,
    val price: Double
)

The primary key uniquely identifies each row. For money calculations, consider using an integer representation in the smallest currency unit or a carefully defined decimal strategy rather than relying on binary floating-point arithmetic.

17. Interview Problem: Create a DAO

import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow

@Dao
interface ProductDao {
    @Query("SELECT * FROM products ORDER BY title ASC")
    fun observeProducts(): Flow<List<ProductEntity>>

    @Query("SELECT * FROM products WHERE id = :id")
    suspend fun getProductById(
        id: Int
    ): ProductEntity?

    @Upsert
    suspend fun saveProducts(
        products: List<ProductEntity>
    )

    @Query("DELETE FROM products WHERE id = :id")
    suspend fun deleteProduct(id: Int)
}

Room supports suspend functions for one-shot reads and writes and Flow for observable queries. The @Upsert annotation is available in supported Room versions; for older versions, select an appropriate insert or update strategy.

18. Interview Problem: What Is the Difference Between Flow and StateFlow?

Flow StateFlow
Represents an asynchronous stream of values. Represents observable state with a current value.
Usually starts emitting when collected, depending on its source. Always has a current value.
May be cold or derived from a cold flow. Is a hot state holder.
Commonly used for database queries and transformations. Commonly used for UI state in ViewModels.
Does not require an initial value for a typical Flow. Requires an initial value.

Example: Exposing UI State with StateFlow

data class ProductUiState(
    val isLoading: Boolean = false,
    val products: List<ProductEntity> = emptyList(),
    val error: String? = null
)

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

    private val _uiState =
        MutableStateFlow(ProductUiState())

    val uiState: StateFlow<ProductUiState> =
        _uiState.asStateFlow()
}

The ViewModel can update _uiState when a request starts, succeeds, or fails. The UI observes uiState and renders the current state.

19. Interview Problem: Integrate API and Room Database

A common interview challenge is to fetch data from a remote API, save it in Room, and display the data from the database. This design supports a consistent local source for the UI and can improve offline behavior.

19.1 Recommended Data Flow

  1. The screen observes a Flow from the local database.
  2. The ViewModel requests a refresh from the repository.
  3. The repository calls the remote API.
  4. The API response is validated and mapped to local entities.
  5. The repository writes the entities to Room.
  6. The Room query emits updated records.
  7. The screen automatically displays the updated data.

19.2 Repository Example

class ProductRepository(
    private val api: ProductApi,
    private val dao: ProductDao
) {
    fun observeProducts(): Flow<List<ProductEntity>> {
        return dao.observeProducts()
    }

    suspend fun refreshProducts(): Result<Unit> {
        return try {
            val response = api.getProducts()

            if (!response.isSuccessful) {
                return Result.failure(
                    IllegalStateException(
                        "HTTP ${response.code()}"
                    )
                )
            }

            val remoteProducts = response.body()
                ?: return Result.failure(
                    IllegalStateException(
                        "Empty product response"
                    )
                )

            val entities = remoteProducts.map { product ->
                ProductEntity(
                    id = product.id,
                    title = product.title,
                    price = product.price
                )
            }

            dao.saveProducts(entities)
            Result.success(Unit)
        } catch (cancelled: CancellationException) {
            throw cancelled
        } catch (exception: Exception) {
            Result.failure(exception)
        }
    }
}

This example assumes that ProductApi and the remote product model are already defined. It demonstrates the main pattern, but a production repository should also define stale-record removal, transaction boundaries, pagination, and conflict-resolution behavior as needed.

20. Interview Problem: Handling Offline Data

Offline-first behavior means the application can display locally available information even when the network is unavailable. The repository can use Room as the UI's data source and attempt a remote refresh separately.

Recommended Approach

  • Display cached data immediately when available.
  • Attempt to refresh the data from the API.
  • Update Room after a successful response.
  • Preserve existing cached data when a refresh fails.
  • Show a message that the displayed information may be outdated.
  • Define a policy for data freshness and cache expiration.
  • For offline edits, track pending changes and synchronize them later.

Offline reading is simpler than offline editing. Offline writes require handling retries, duplicate requests, record conflicts, and server-side validation.

21. Interview Problem: Database Transactions

A transaction groups related database operations so they succeed or fail together. This is important when multiple changes must preserve a consistent state.

Example

Suppose an expense tracker saves a transaction and updates a monthly summary. If both changes must remain consistent, a transaction can prevent one update from being committed without the other.

import androidx.room.withTransaction

suspend fun saveExpenseAndSummary(
    database: AppDatabase,
    expense: ExpenseEntity,
    summary: MonthlySummaryEntity
) {
    database.withTransaction {
        database.expenseDao().insert(expense)
        database.summaryDao().upsert(summary)
    }
}

This example assumes the database exposes the relevant DAOs and the entities are defined. Use a transaction when multiple related database changes must be atomic. Do not wrap unrelated remote network calls inside a database transaction.

22. Interview Problem: Database Migration

A migration updates the database schema when the application changes between versions. For example, a new application version may add a category column to an existing products table.

Important Migration Practices

  • Increase the database version when the schema changes.
  • Define a migration for supported existing database versions.
  • Preserve user data whenever possible.
  • Test migrations using databases created with earlier schemas.
  • Avoid destructive migrations unless data loss is explicitly acceptable.
  • Export and maintain schema information as part of the project.

Interviewers may ask how to add a non-null column to an existing table. The migration must account for existing rows, often by providing a valid default value or a deliberate data transformation.

23. Interview Problem: Prevent Duplicate API Requests

Duplicate requests can occur when users tap a button repeatedly, a screen recomposes, or multiple components trigger the same operation. The solution depends on the use case.

  • Expose a loading state and disable repeated user submissions.
  • Keep network requests in the ViewModel or repository rather than triggering them directly during UI rendering.
  • Use request deduplication or caching where appropriate.
  • Use backend-supported idempotency keys for operations where duplicate writes would be harmful.
  • Cancel obsolete requests when newer work replaces them.

Example

fun refreshProducts() {
    if (_uiState.value.isLoading) return

    viewModelScope.launch {
        _uiState.update {
            it.copy(isLoading = true, error = null)
        }

        try {
            val result = repository.refreshProducts()

            _uiState.update { current ->
                result.fold(
                    onSuccess = {
                        current.copy(isLoading = false)
                    },
                    onFailure = { error ->
                        current.copy(
                            isLoading = false,
                            error = error.message
                                ?: "Unable to refresh products"
                        )
                    }
                )
            }
        } catch (cancelled: CancellationException) {
            throw cancelled
        }
    }
}

This example assumes the ViewModel defines a compatible mutable UI state and that required imports such as update, viewModelScope, and CancellationException are included. For concurrent calls that can enter simultaneously, use an appropriate mutex or request-management strategy rather than relying only on a state check.

24. Interview Problem: Pagination

Pagination loads a large dataset in smaller portions. It reduces the amount of data transferred and can improve loading speed and memory usage.

Common Pagination Approaches

  • Page number: Requests data using a page and page size.
  • Offset and limit: Requests records from a specified offset.
  • Cursor-based: Requests the next set of records using a server-provided cursor.

For interview problems, discuss loading indicators, end-of-list detection, duplicate prevention, retry behavior, and what happens when a refresh occurs during pagination. Android's Paging library can help manage more complex paging workflows.

25. Interview Problem: Handling API Rate Limits and Retries

Retrying every failed request immediately can worsen a server outage or violate rate limits. A robust implementation distinguishes retryable failures from permanent failures.

  • Retry transient network failures when the operation is safe to repeat.
  • Use exponential backoff where appropriate.
  • Respect the server's retry guidance, including a valid Retry-After value.
  • Avoid automatically retrying non-idempotent operations without a safe strategy.
  • Limit the number of retries.
  • Show an understandable error if the request still fails.

For example, a GET request may be safe to retry, while repeating a payment or order-creation POST could create duplicate transactions unless the backend provides idempotency support.

26. Interview Problem: Managing Loading, Success, and Error States

A screen should represent the status of an operation clearly. Separate UI states help prevent inconsistent combinations, such as showing a spinner after an error or displaying stale error messages after a successful refresh.

sealed interface UiState<out T> {
    data object Loading : UiState<Nothing>
    data class Success<T>(
        val data: T
    ) : UiState<T>
    data class Error(
        val message: String
    ) : UiState<Nothing>
}

This sealed-state pattern is useful for simple screens. For screens that need to display cached data while refreshing, a data class containing data, loading status, and an optional error may better represent the required behavior.

27. Interview Problem: Avoiding Memory Leaks

Memory leaks can occur when objects retain references longer than their intended lifecycle. Coroutine lifecycle mistakes may also cause unnecessary work to continue after a screen is gone.

  • Use viewModelScope for ViewModel-owned operations.
  • Use lifecycle-aware collection for UI state.
  • Avoid retaining Activity or View references in long-lived objects.
  • Do not use global coroutine scopes for screen-specific tasks.
  • Cancel callbacks and subscriptions when they are no longer needed.
  • Keep repositories independent of Activity lifecycle objects.

28. Interview Problem: Testing Coroutines and Repositories

Tests should verify behavior without relying on real network services or unpredictable timing. Fake repositories and mock API implementations allow the test to control success, failure, and delayed responses.

Testing Checklist

  • Verify that a successful API response is mapped correctly.
  • Verify that HTTP failures produce the expected result.
  • Verify that network exceptions are handled safely.
  • Verify that cancellation is propagated.
  • Verify that Room inserts and queries return expected data.
  • Verify that a refresh updates observable UI state.
  • Verify that retries do not duplicate writes.
  • Use coroutine test utilities to control dispatchers and virtual time.

For coroutine tests, inject dispatchers when useful and use a shared test scheduler so asynchronous operations can be advanced deterministically.

29. Common Interview Mistakes

Mistake Better Approach
Calling blocking work on the main thread. Use a main-safe suspending function or move blocking work to an appropriate dispatcher.
Using GlobalScope for screen operations. Use lifecycle-appropriate scopes such as viewModelScope.
Using async for every coroutine. Use async when the result is needed; otherwise use launch.
Ignoring HTTP error codes. Handle success, HTTP errors, and network exceptions separately.
Updating UI directly from a repository. Expose results or state to the ViewModel and let the UI observe them.
Making the UI depend on API responses only. Use a repository and local database when caching or offline access is required.
Using destructive migrations casually. Define and test migrations that preserve data where required.
Catching every exception without rethrowing cancellation. Propagate CancellationException and handle expected failures explicitly.
Storing passwords in plain text. Use backend authentication and appropriate secure session management.
Retrying every failed request automatically. Retry only suitable operations with limits and backoff.

30. Practical Coding Challenges

Challenge 1: Fetch and Display Users

Create a Retrofit API interface that retrieves a list of users. Display a loading indicator while the request is running, show the result on success, and display a retry option when the request fails.

Challenge 2: Cache API Results in Room

Fetch a product list from an API, map the response into Room entities, and display products by observing a database Flow.

Challenge 3: Search with Debounce

Build a search screen that waits briefly after the user's last keystroke before calling the API. Ensure obsolete requests do not overwrite the newest search results.

Challenge 4: Parallel Requests

Fetch a user profile and a notification count concurrently using async and await. Decide how the UI should behave if one request fails and the other succeeds.

Challenge 5: Offline-First Notes

Build a notes application that saves notes locally, displays them without internet access, and synchronizes pending changes when the network becomes available.

Challenge 6: Database Migration

Add a new column to an existing Room entity and create a migration that preserves previously saved records.

31. Frequently Asked Interview Questions and Answers

Q1. What is a coroutine?

Answer: A coroutine is a suspendable computation that can pause and resume without blocking the underlying thread while waiting for asynchronous work.

Q2. What is structured concurrency?

Answer: Structured concurrency organizes related coroutines into a parent-child hierarchy, helping manage their lifecycle, completion, cancellation, and failures.

Q3. Can a suspend function run on the main thread?

Answer: Yes. The suspend keyword alone does not change the dispatcher. A suspending function should be main-safe so that callers can invoke it safely from the main context.

Q4. What is the difference between launch and async?

Answer: Launch returns a Job and is commonly used for operations without a direct result. Async returns a Deferred, whose result can be obtained using await.

Q5. What is the purpose of withContext?

Answer: It executes a suspending block in a specified coroutine context and returns the block's result when it completes.

Q6. What is Retrofit?

Answer: Retrofit is a type-safe HTTP client that creates implementations of annotated API interfaces and supports suspending request functions.

Q7. What is an HTTP interceptor?

Answer: An interceptor can inspect or modify HTTP requests and responses, such as attaching an authorization header or collecting sanitized diagnostic information.

Q8. What is Room Database?

Answer: Room is an Android persistence library built on SQLite that provides entities, DAOs, and a database class.

Q9. What is the difference between Flow and StateFlow?

Answer: Flow represents an asynchronous stream, while StateFlow is a hot state holder that always exposes a current value.

Q10. How do you combine API and database operations?

Answer: A repository fetches data from the API, maps it to database entities, stores it in Room, and exposes a local Flow to the UI.

Q11. How do you handle an API error?

Answer: Check the HTTP response, distinguish expected server errors from network failures, map them to suitable application-level errors, and update the UI state.

Q12. What is a database transaction?

Answer: A transaction groups related database operations so they either all succeed or all roll back, helping preserve data consistency.

Q13. What is database migration?

Answer: A migration updates the database schema between versions while preserving existing data according to the migration rules.

Q14. What is debouncing?

Answer: Debouncing delays an action until a stream of events has paused for a specified interval. It is useful for search fields to reduce unnecessary API requests.

Q15. How do you prevent duplicate network requests?

Answer: Use loading-state guards, request deduplication, caching, cancellation, or backend-supported idempotency according to the operation.

Q16. How do you support offline access?

Answer: Cache suitable data locally in Room, observe the database for UI updates, and define synchronization rules for refreshing or submitting pending changes.

Q17. Why should CancellationException be rethrown?

Answer: Cancellation is part of structured concurrency. Swallowing cancellation can prevent work from stopping when its result is no longer needed.

Q18. How do you test asynchronous code?

Answer: Use coroutine test utilities, controlled dispatchers, fake repositories, and mock API responses to verify success, failure, and cancellation behavior deterministically.

32. Final Revision Checklist

  • Understand suspend functions, launch, async, await, and withContext.
  • Explain coroutine scopes, dispatchers, cancellation, and structured concurrency.
  • Know how Retrofit interfaces and HTTP response handling work.
  • Understand authorization headers and safe token handling.
  • Create Room entities, DAOs, and database migrations.
  • Explain the difference between Flow and StateFlow.
  • Design a repository that combines API data with local persistence.
  • Handle loading, empty, success, and error states.
  • Understand caching, offline behavior, pagination, and retries.
  • Write tests for repositories, database operations, and coroutine behavior.
  • Explain trade-offs clearly instead of memorizing code alone.

33. Conclusion

Solving coroutine, API integration, and database interview problems requires understanding how asynchronous tasks, remote services, local persistence, and UI state work together. Kotlin coroutines help organize asynchronous operations, Retrofit simplifies HTTP communication, Room provides structured local storage, and repositories coordinate data access.

During interviews, explain not only how a solution works but also why it is appropriate. Discuss cancellation, error handling, data consistency, security, testing, and offline behavior. Practicing these concepts through small projects will help you solve practical Android development problems and prepare for technical interviews.


Related Learning Resources

Previous Next
whatsapp