Unit 5 Pathway 1 Activity 3: Introduction to Coroutines in Android Studio¶

Before you begin¶

  • In the previous codelab, you learned about coroutines. You used Kotlin Playground to write concurrent code using coroutines. In this codelab, you’ll apply your knowledge of coroutines within an Android app and its lifecycle. You’ll add code to launch new coroutines concurrently and learn how to test them.

Prerequisites¶

  • Knowledge of Kotlin language basics, including functions and lambdas

  • Able to build layouts in Jetpack Compose

  • Able to write unit tests in Kotlin (refer to Write unit tests for ViewModel codelab)

  • How threads and concurrency work

  • Basic knowledge of coroutines and CoroutineScope

What you’ll build¶

  • Race Tracker app that simulates race progress between two players. Consider this app as a chance to experiment and learn more about different aspects of coroutines.

What you’ll learn¶

  • Using coroutines in the Android app lifecycle.

  • Principles of structured concurrency.

What you’ll need¶

  • The latest stable version of Android Studio

App overview¶

  • The Race Tracker app simulates two players running a race. The app UI consists of two buttons, Start/Pause and Reset, and two progress bars to show the progress of the racers. Players 1 and 2 are set to “run” the race at different speeds. When the race starts Player 2 progresses twice as fast as Player 1.

  • Coroutines are used to ensure:

    • Both players “run the race” concurrently.

    • The app UI is responsive and the progress bars increments during the race.

  • The starter code has the UI code ready for the Race Tracker app. The main focus of this part of the codelab is to get you familiar with Kotlin coroutines inside an Android app.

Get the starter code¶

Starter code walkthrough¶

  • You can start the race by clicking the Start button. The text of the Start button changes to Pause while the race is in progress.

    ../_images/unit5-pathway1-activity3-section2-2ee492f277625f0a_1440.png
  • At any point in time, you can use this button to pause or continue the race.

    ../_images/unit5-pathway1-activity3-section2-50e992f4cf6836b7_1440.png
  • When the race starts, you can see the progress of each player through a status indicator. The StatusIndicator composable function displays the progress status of each player. It uses the LinearProgressIndicator composable to display the progress bar. You’ll be using coroutines to update the value for progress.

    ../_images/unit5-pathway1-activity3-section2-79cf74d82eacae6f_1440.png
  • RaceParticipant.kt âžś class RaceParticipant provides the data for progress increment. This class is a state holder for each of the players. It maintains:

    • the name of the participant

    • the maxProgress to reach to finish the race

    • the delay duration between progress increments

    • currentProgress in race

    • the initialProgress

  • In the next section, you will use coroutines to implement the functionality to simulate the race progress without blocking the app UI.

Implement race progress¶

  • In RaceParticipant.kt âžś class RaceParticipant, add a run() function:

      class RaceParticipant(
          val name: String,
          val maxProgress: Int = 100,
          val progressDelayMillis: Long = 500L,
          private val progressIncrement: Int = 1,
          private val initialProgress: Int = 0
      ) {
          init {
              require(maxProgress > 0) { "maxProgress=$maxProgress; must be > 0" }
              require(progressIncrement > 0) { "progressIncrement=$progressIncrement; must be > 0" }
          }
    
          /**
          * Indicates the race participant's current progress
          */
          var currentProgress by mutableStateOf(initialProgress)
              private set
    
          suspend fun run() {
              while (currentProgress < maxProgress) {
    
                  // Simulate different progress intervals in the race
                  delay(progressDelayMillis)
    
                  // Simulate the runner's progress
                  currentProgress += progressIncrement
              }
          }
    
          /**
          * Regardless of the value of [initialProgress] the reset function will reset the
          * [currentProgress] to 0
          */
          fun reset() {
              currentProgress = 0
          }
      }
    

    imports

    import kotlinx.coroutines.delay
    
  • When you look at the code you just added, you will see an icon on the left of the call to the delay() function in Android Studio, as shown in the screenshot below:

    ../_images/unit5-pathway1-activity3-section3-11b5df57dcb744dc_1440.png
  • This icon indicates the suspension point when the function might suspend and resume again later.

  • The main thread is not blocked while the coroutine is waiting to complete the delay duration, as shown in the following diagram:

    ../_images/unit5-pathway1-activity3-section3-a3c314fb082a9626_1440.png
  • The coroutine suspends (but doesn’t block) the execution after calling the delay() function with the desired interval value. Once the delay is complete, the coroutine resumes the execution and updates the value of the currentProgress property.

Start the race¶

  • In RaceTrackerApp.kt âžś RaceTrackerApp(), add this code:

    @Composable
    fun RaceTrackerApp() {
        /**
        * Note: To survive the configuration changes such as screen rotation, [rememberSaveable] should
        * be used with custom Saver object. But to keep the example simple, and keep focus on
        * Coroutines that implementation detail is stripped out.
        */
        val playerOne = remember {
            RaceParticipant(name = "Player 1", progressIncrement = 1)
        }
        val playerTwo = remember {
            RaceParticipant(name = "Player 2", progressIncrement = 2)
        }
        var raceInProgress by remember { mutableStateOf(false) }
    
        // added code begin
        if (raceInProgress) {
            LaunchedEffect(playerOne, playerTwo) {
                coroutineScope {
                    launch { playerOne.run() }
                    launch { playerTwo.run() }
                }
                raceInProgress = false
            }
        }
        // added code end
    
        RaceTrackerScreen(
            playerOne = playerOne,
            playerTwo = playerTwo,
            isRunning = raceInProgress,
            onRunStateChange = { raceInProgress = it },
            modifier = Modifier
                .statusBarsPadding()
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
                .safeDrawingPadding()
                .padding(horizontal = dimensionResource(R.dimen.padding_medium)),
        )
    }
    

    imports

    import androidx.compose.runtime.LaunchedEffect
    
  • When the user presses Start, raceInProgress is set to true.

  • The code inside the if (raceInProgress) block runs.

  • The LaunchedEffect() function enters the Composition.

  • The coroutineScope() launches 2 coroutines concurrently. It ensures that both coroutines complete execution, before updating the raceInProgress flag.

  • When raceInProgress is false, the LaunchedEffect() exits the composition. The 2 coroutines are canceled.

Structured concurrency¶

  • The way you write code using coroutines is called structured concurrency. The idea is that coroutines have a hierarchy — tasks might launch subtasks, which might launch subtasks in turn. The unit of this hierarchy is referred to as a coroutine scope. Coroutine scopes should always be associated with a lifecycle.

  • The Coroutines APIs adhere to this structured concurrency by design. You cannot call a suspend function from a function which is not marked suspend. This limitation ensures that you call the suspend functions from coroutine builders, such as launch. These builders are, in turn, tied to a CoroutineScope.

  • Run the app.

    ../_images/unit5-pathway1-activity3-section6-598ee57f8ba58a52_1440.png
  • Click the Start button. Player 2 runs faster than Player 1. After the race is complete, which is when both players reach 100% progress, the label for the Pause button changes to Start. You can click the Reset button to reset the race and re-execute the simulation. The race is shown in the following video.

    ../_images/unit5-pathway1-activity3-section6-c1035eecc5513c58.gif
  • The execution flow is shown in the following diagram.

    ../_images/unit5-pathway1-activity3-section6-cf724160fd66ff21_1440.png
  • When the LaunchedEffect() block executes, the control is transferred to the coroutineScope{..} block.

  • The coroutineScope block launches both coroutines concurrently and waits for them to finish execution.

  • Once the execution is complete, the raceInProgress flag updates.

  • The coroutineScope block only returns and moves on after all the code inside the block completes execution. For the code outside of the block, the presence or absence of concurrency becomes a mere implementation detail. This coding style provides a structured approach to concurrent programming and is referred to as structured concurrency.

  • When you click the Reset button after the race completes, the coroutines are canceled, and the progress for both players is reset to 0.

Solution code¶

Conclusion¶

  • Congratulations! You just learned how to use coroutines to handle concurrency. Coroutines help manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive. You also learned how to write unit tests to test the coroutines.

  • The following features are some of the benefits of coroutines:

    • Readability: The code you write with coroutines provides a clear understanding of the sequence that executes the lines of code.

    • Jetpack integration: Many Jetpack libraries, such as Compose and ViewModel, include extensions that provide full coroutines support. Some libraries also provide their own coroutine scope that you can use for structured concurrency.

    • Structured concurrency: Coroutines make concurrent code safe and easy to implement, eliminate unnecessary boilerplate code, and ensure that coroutines launched by the app are not lost or keep wasting resources.

  • Coroutines enable you to write long running code that runs concurrently without learning a new style of programming. The execution of a coroutine is sequential by design.

  • The suspend keyword is used to mark a function, or function type, to indicate its availability to execute, pause, and resume a set of code instructions.

  • A suspend function can be called only from another suspend function.

  • You can start a new coroutine using the launch or async builder function.

  • Coroutine context, coroutine builders, Job, coroutine scope and Dispatcher are the major components for implementing coroutines.

  • Coroutines use dispatchers to determine the threads to use for its execution.

  • Job plays an important role to ensure structured concurrency by managing the lifecycle of coroutines and maintaining the parent-child relationship.

  • A CoroutineContext defines the behavior of a coroutine using Job and a coroutine dispatcher.

  • A CoroutineScope controls the lifetime of coroutines through its Job and enforces cancellation and other rules to its children and their children recursively.

  • Launch, completion, cancellation, and failure are four common operations in the coroutine’s execution.

  • Coroutines follow a principle of structured concurrency.