Bird
Raised Fist0
Android Kotlinmobile~20 mins

Coroutines for async networking in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: User List Screen
This screen fetches a list of users from a network API asynchronously using Kotlin coroutines and displays them in a scrollable list.
Target UI
-------------------------
|      User List        |
-------------------------
| Loading...            |
|                       |
|                       |
|                       |
|                       |
-------------------------
Use Kotlin coroutines to fetch data asynchronously from a fake API endpoint.
Show a loading indicator while fetching data.
Display the list of user names in a vertical scrollable list after loading.
Handle errors by showing a simple error message on the screen.
Use ViewModel and LiveData or StateFlow to manage UI state.
Starter Code
Android Kotlin
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class UserListViewModel : ViewModel() {
    // TODO: Add state variables for loading, users list, and error message

    fun fetchUsers() {
        // TODO: Implement coroutine to fetch users asynchronously
    }
}

class UserListActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: android.os.Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel = UserListViewModel()
        setContent {
            UserListScreen(viewModel)
        }
        viewModel.fetchUsers()
    }
}

@Composable
fun UserListScreen(viewModel: UserListViewModel) {
    // TODO: Observe state and display loading, error or user list
}
Task 1
Task 2
Task 3
Task 4
Task 5
Task 6
Solution
Android Kotlin
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class UserListViewModel : ViewModel() {
    var loading by mutableStateOf(false)
        private set
    var users by mutableStateOf(listOf<String>())
        private set
    var error by mutableStateOf<String?>(null)
        private set

    fun fetchUsers() {
        loading = true
        error = null
        viewModelScope.launch {
            try {
                delay(2000) // Simulate network delay
                // Simulate fetched user names
                users = listOf("Alice", "Bob", "Charlie", "Diana")
            } catch (e: Exception) {
                error = "Failed to load users"
            } finally {
                loading = false
            }
        }
    }
}

class UserListActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: android.os.Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel = UserListViewModel()
        setContent {
            UserListScreen(viewModel)
        }
        viewModel.fetchUsers()
    }
}

@Composable
fun UserListScreen(viewModel: UserListViewModel) {
    val loading = viewModel.loading
    val users = viewModel.users
    val error = viewModel.error

    Surface(modifier = Modifier.fillMaxSize()) {
        when {
            loading -> {
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
                    CircularProgressIndicator()
                }
            }
            error != null -> {
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
                    Text(text = error, color = MaterialTheme.colorScheme.error)
                }
            }
            users.isNotEmpty() -> {
                LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
                    items(users) { user ->
                        Text(text = user, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(vertical = 8.dp))
                    }
                }
            }
            else -> {
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
                    Text(text = "No users found.")
                }
            }
        }
    }
}

This solution uses Kotlin coroutines inside the ViewModel to fetch user data asynchronously. The fetchUsers() function launches a coroutine in the viewModelScope, simulating a network delay with delay(2000). It updates the loading, users, and error state variables accordingly.

The UI is built with Jetpack Compose. The UserListScreen composable observes these state variables and shows a loading spinner while loading, an error message if there is an error, or a scrollable list of user names when data is loaded. This separation keeps UI reactive and simple.

This approach teaches beginners how to use coroutines for async networking, manage UI state with Compose, and handle loading and error states gracefully.

Final Result
Completed Screen
-------------------------
|      User List        |
-------------------------
| • Alice               |
| • Bob                 |
| • Charlie             |
| • Diana               |
|                       |
-------------------------
When the screen opens, a loading spinner appears for 2 seconds.
After loading, the list of user names is shown in a scrollable list.
If an error occurs during fetch, an error message is displayed instead.
Stretch Goal
Add a retry button that appears when an error occurs, allowing the user to try fetching the users again.
💡 Hint
Show a Button below the error message that calls fetchUsers() again when clicked.

Practice

(1/5)
1. What is the main benefit of using coroutines for network calls in Android apps?
easy
A. They replace the need for any background threads.
B. They make the app run faster by using more CPU cores.
C. They automatically cache network data for offline use.
D. They prevent the app's screen from freezing during network tasks.

Solution

  1. Step 1: Understand what happens during network calls

    Network calls can take time and if done on the main thread, they freeze the UI.
  2. Step 2: Role of coroutines in network calls

    Coroutines run network tasks in the background, keeping the UI responsive.
  3. Final Answer:

    They prevent the app's screen from freezing during network tasks. -> Option D
  4. Quick Check:

    Coroutines keep UI responsive = D [OK]
Hint: Coroutines keep UI smooth by running tasks in background [OK]
Common Mistakes:
  • Thinking coroutines speed up CPU usage
  • Believing coroutines cache data automatically
  • Assuming coroutines remove all threading needs
2. Which of the following is the correct way to start a coroutine for a network call on a background thread?
easy
A. launch(Dispatchers.Main) { fetchData() }
B. launch(Dispatchers.Default) { updateUI() }
C. launch(Dispatchers.IO) { fetchData() }
D. launch { fetchData() }

Solution

  1. Step 1: Identify dispatcher for background network tasks

    Dispatchers.IO is designed for blocking IO tasks like network calls.
  2. Step 2: Match correct coroutine launch syntax

    launch(Dispatchers.IO) { fetchData() } runs fetchData() on IO thread pool.
  3. Final Answer:

    launch(Dispatchers.IO) { fetchData() } -> Option C
  4. Quick Check:

    Network calls use Dispatchers.IO = B [OK]
Hint: Use Dispatchers.IO for network calls in launch [OK]
Common Mistakes:
  • Using Dispatchers.Main for network calls
  • Confusing Dispatchers.Default with IO
  • Omitting dispatcher and running on main thread
3. What will be the output of this code snippet?
fun main() = runBlocking {
  launch(Dispatchers.IO) {
    println("Network call started")
  }
  println("Main thread continues")
}
medium
A. Main thread continues\nNetwork call started
B. Only Network call started
C. Network call started\nMain thread continues
D. Only Main thread continues

Solution

  1. Step 1: Understand runBlocking and launch behavior

    runBlocking blocks main thread until all coroutines complete. launch starts coroutine asynchronously.
  2. Step 2: Order of print statements

    "Main thread continues" prints immediately; "Network call started" prints shortly after from IO dispatcher.
  3. Final Answer:

    Main thread continues\nNetwork call started -> Option A
  4. Quick Check:

    launch is async, main prints first = C [OK]
Hint: launch runs async, main prints before coroutine output [OK]
Common Mistakes:
  • Assuming launch blocks main thread
  • Expecting network print before main print
  • Ignoring coroutine dispatchers
4. Identify the error in this coroutine code snippet:
GlobalScope.launch(Dispatchers.IO) {
  val data = fetchData()
  textView.text = data
}
medium
A. fetchData() must be called on Dispatchers.Main.
B. Cannot update UI from Dispatchers.IO thread.
C. GlobalScope cannot be used with launch.
D. textView.text should be assigned inside fetchData().

Solution

  1. Step 1: Check thread for UI update

    UI elements like textView must be updated on the main thread.
  2. Step 2: Identify misuse of Dispatchers.IO

    Dispatchers.IO runs on background thread; updating UI here causes error.
  3. Final Answer:

    Cannot update UI from Dispatchers.IO thread. -> Option B
  4. Quick Check:

    UI updates require Dispatchers.Main = A [OK]
Hint: Update UI only on Dispatchers.Main thread [OK]
Common Mistakes:
  • Updating UI on background thread
  • Misusing GlobalScope without lifecycle awareness
  • Calling fetchData() on main thread unnecessarily
5. You want to fetch user data from the network and then update the UI with the result. Which coroutine structure correctly handles this?
hard
A. launch(Dispatchers.Main) { val data = withContext(Dispatchers.IO) { fetchUser() }; updateUI(data) }
B. launch(Dispatchers.IO) { val data = fetchUser(); updateUI(data) }
C. launch { val data = fetchUser(); updateUI(data) }
D. launch(Dispatchers.Default) { val data = fetchUser(); updateUI(data) }

Solution

  1. Step 1: Identify correct dispatcher for network call

    Network call fetchUser() should run on Dispatchers.IO to avoid blocking UI.
  2. Step 2: Ensure UI update runs on main thread

    updateUI(data) must run on Dispatchers.Main to safely update UI elements.
  3. Step 3: Analyze launch(Dispatchers.Main) { val data = withContext(Dispatchers.IO) { fetchUser() }; updateUI(data) } structure

    launch on Main dispatcher, inside withContext(Dispatchers.IO) runs fetchUser(), then UI updates on Main.
  4. Final Answer:

    launch(Dispatchers.Main) { val data = withContext(Dispatchers.IO) { fetchUser() }; updateUI(data) } -> Option A
  5. Quick Check:

    Network on IO, UI on Main = A [OK]
Hint: Use withContext(IO) inside Main launch for network + UI update [OK]
Common Mistakes:
  • Updating UI on IO or Default dispatcher
  • Running network call on Main dispatcher directly
  • Not switching context for network call
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard