1. Welcome
Introduction
One of the top priorities for creating a flawless user experience is ensuring the UI of your app is always responsive and running smoothly. To deliver this experience, we can improve UI performance by moving long-running tasks, such as database operations, into the background.
In this codelab, you implement the user-facing portion of the TrackMySleepQuality app, using Kotlin coroutines to perform database operations away from the main thread.
What you should already know
You should be familiar with:
- Building a basic user interface (UI) using an activity, fragments, views, and click handlers.
- Navigating between fragments, and using
safeArgs
to pass simple data between fragments. - View models, view model factories, transformations, and
LiveData
. - How to create a
Room
database, create a DAO, and define entities. - Basic threading and multiprocessing concepts.
What you'll learn
- How threads work in Android.
- How to use Kotlin coroutines to move database operations away from the main thread.
- How to display formatted data in a
TextView
.
What you'll do
- Extend the TrackMySleepQuality app to collect, store, and display data in and from the database.
- Use coroutines to run long-running database operations in the background.
- Use
LiveData
to trigger navigation and the display of a snackbar. - Use
LiveData
to enable and disable buttons.
2. App overview
In this codelab, you build the view model, coroutines, and data-display portions of the TrackMySleepQuality app.
The app has two screens, represented by fragments, as shown in the figure below.
The first screen, shown on the left, has buttons to start and stop tracking. The screen shows all the user's sleep data. The Clear button permanently deletes all the data that the app has collected for the user.
The second screen, shown on the right, is for selecting a sleep-quality rating. In the app, the rating is represented numerically. For development purposes, the app shows both the face icons and their numerical equivalents.
The user's flow is as follows:
- User opens the app and is presented with the sleep-tracking screen.
- User taps the Start button. This records the starting time and displays it. The Start button is disabled, and the Stop button is enabled.
- User taps the Stop button. This records the ending time and opens the sleep-quality screen.
- User selects a sleep-quality icon. The screen closes, and the tracking screen displays the sleep-ending time and sleep quality. The Stop button is disabled and the Start button is enabled. The app is ready for another night.
- The Clear button is enabled whenever there is data in the database. When the user taps the Clear button, all their data is erased without recourse—there is no "Are you sure?" message.
This app uses a simplified architecture, as shown below in the context of the full architecture. The app uses only the following components:
- UI controller
- View model and
LiveData
- A Room database
3. Task: Inspect the starter code
In this task, you use a TextView
to display formatted sleep tracking data. (This is not the final user interface. You will improve the UI in another codelab.)
You can continue with the TrackMySleepQuality app that you built in the previous codelab or download the starter app for this codelab.
Step 1: Download and run the starter app
- Download the TrackMySleepQuality-Coroutines-Starter app from GitHub.
- Build and run the app. The app shows the UI for the
SleepTrackerFragment
fragment, but no data. The buttons do not respond to taps.
Step 2: Inspect the code
The starter code for this codelab is the same as the solution code for the Create a Room Database codelab.
- Open res/layout/activity_main.xml. This layout contains the
nav_host_fragment
fragment. Also, notice the<merge>
tag. Themerge
tag can be used to eliminate redundant layouts when including layouts, and it's a good idea to use it. An example of a redundant layout would be ConstraintLayout > LinearLayout > TextView, where the system might be able to eliminate the LinearLayout. This kind of optimization can simplify the view hierarchy and improve app performance. - In the navigation folder, open navigation.xml. You can see two fragments and the navigation actions that connect them.
- In the layout folder, open fragment_sleep_tracker.xml and click on the Code view to see its XML layout. Notice the following:
- The layout data is wrapped in a
<layout>
element to enable data binding. ConstraintLayout
and the other views are arranged inside the<layout>
element.- The file has a placeholder
<data>
tag.
The starter app also provides dimensions, colors, and styling for the UI. The app contains a Room
database, a DAO, and a SleepNight
entity. If you did not complete the preceding Create a Room Database codelab, make sure you explore these aspects of the code on your own.
4. Task: Add a ViewModel
Now that you have a database and a UI, you need to collect data, add the data to the database, and display the data. All this work is done in the view model. Your sleep-tracker view model will handle button clicks, interact with the database via the DAO, and provide data to the UI via LiveData
. All database operations will have to be run away from the main UI thread, and you'll do that using coroutines.
Step 1: Add SleepTrackerViewModel
- In the sleeptracker package, open SleepTrackerViewModel.kt.
- Inspect the
SleepTrackerViewModel
class, which is provided for you in the starter app and is also shown below. Note that the class extendsAndroidViewModel
. This class is the same asViewModel
, but it takes the application context as a constructor parameter and makes it available as a property. You will need this later.
class SleepTrackerViewModel(
val database: SleepDatabaseDao,
application: Application) : AndroidViewModel(application) {
}
Step 2: Add SleepTrackerViewModelFactory
- In the sleeptracker package, open SleepTrackerViewModelFactory.kt.
- Examine the code that's provided for you for the factory, which is shown below:
class SleepTrackerViewModelFactory(
private val dataSource: SleepDatabaseDao,
private val application: Application) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
return SleepTrackerViewModel(dataSource, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Take note of the following:
- The provided
SleepTrackerViewModelFactory
takes the same argument as theViewModel
and extendsViewModelProvider.Factory
. - Inside the factory, the code overrides
create()
, which takes any class type as an argument and returns aViewModel
. - In the body of
create()
, the code checks that there is aSleepTrackerViewModel
class available, and if there is, returns an instance of it. Otherwise, the code throws an exception.
Step 3: Update SleepTrackerFragment
- In
SleepTrackerFragment.kt
, get a reference to the application context. Put the reference inonCreateView()
, belowbinding
. You need a reference to the app that this fragment is attached to, to pass into the view-model factory provider.
The requireNotNull
Kotlin function throws an IllegalArgumentException
if the value is null
.
val application = requireNotNull(this.activity).application
- You need a reference to your data source via a reference to the DAO. In
onCreateView()
, before thereturn
, define adataSource
. To get a reference to the DAO of the database, useSleepDatabase.getInstance(application).sleepDatabaseDao
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- In
onCreateView()
, before thereturn
, create an instance of theviewModelFactory
. You need to pass itdataSource
and theapplication
.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
- Now that you have a factory, get a reference to the
SleepTrackerViewModel
. TheSleepTrackerViewModel::class.java
parameter refers to the runtime Java class of this object.
val sleepTrackerViewModel =
ViewModelProvider(
this, viewModelFactory).get(SleepTrackerViewModel::class.java)
- Your finished code should look like this:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
ViewModelProvider(
this, viewModelFactory).get(SleepTrackerViewModel::class.java)
Here's the onCreateView()
method so far:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Get a reference to the binding object and inflate the fragment views.
val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_sleep_tracker, container, false)
val application = requireNotNull(this.activity).application
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
val sleepTrackerViewModel =
ViewModelProvider(
this, viewModelFactory).get(SleepTrackerViewModel::class.java)
return binding.root
}
Step 4: Add data binding for the view model
With the basic ViewModel
in place, you need to finish setting up data binding in the SleepTrackerFragment
to connect the ViewModel
with the UI.
In the fragment_sleep_tracker.xml
layout file:
- Inside the
<data>
section, create a<variable>
that references theSleepTrackerViewModel
class.
<data>
<variable
name="sleepTrackerViewModel"
type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>
In SleepTrackerFragment.kt
:
- Set the current activity as the lifecycle owner of the binding. Add this code inside the
onCreateView()
method, before thereturn
statement:
binding.setLifecycleOwner(this)
- Assign the
sleepTrackerViewModel
binding variable to thesleepTrackerViewModel
. Put this code insideonCreateView()
, below the code that creates theSleepTrackerViewModel
:
binding.sleepTrackerViewModel = sleepTrackerViewModel
- You may see an error, because you have to recreate the binding object. Clean and rebuild the project to get rid of the error.
- Finally, as always, make sure your code builds and runs without errors.
5. Concept: Coroutines
In Kotlin, coroutines are the way to handle long-running tasks elegantly and efficiently instead of callbacks. Kotlin coroutines let you convert callback-based code to sequential code. Code written sequentially is typically easier to read and maintain. Unlike callbacks, coroutines can safely use valuable language features such as exceptions. And most importantly coroutines have a higher degree of maintainability and flexibility. In the end, coroutines and callbacks perform the same functionality: they are both ways of handling potentially long-running asynchronous tasks within an app.
Coroutines have the following properties:
- Coroutines are asynchronous and non-blocking.
- Coroutines use suspend functions to make asynchronous code sequential.
Coroutines are asynchronous.
A coroutine runs independently from the main execution steps of your program. This could be in parallel or on a separate processor. It could also be that while the rest of the app is waiting for input, you sneak in a bit of processing. One of the important aspects of asynchronous programming is you cannot expect the result to be immediately available, until you explicitly wait for it.
For example, let's say you have a question that requires research, and you politely request to ask a colleague to find the answer. Your colleague then starts to work on it by themselves. You can continue to do other unrelated work that doesn't depend on the answer until your colleague returns with an answer. In this example, your colleague is doing the work asynchronously "on a separate thread".
Coroutines are non-blocking.
Non-blocking means that a coroutine does not block, or interfere with the progress of the main or UI thread. So with coroutines, users can have the smoothest possible experience, because the UI interaction, which is run on the main thread, always has priority.
Coroutines use suspend functions to make asynchronous code sequential.
The keyword suspend
is Kotlin's way of marking a function, or function type, as being available to coroutines. When a coroutine calls a function marked with suspend
, instead of blocking until the function returns like a normal function call, the coroutine suspends execution until the result is ready. Then the coroutine resumes where it left off, with the result.
While the coroutine is suspended and waiting for a result, it unblocks the thread that it's running on. That way, other functions or coroutines can run.
The suspend
keyword doesn't specify the thread that the code runs on. A suspend function may run on a background thread, or on the main thread.
To use coroutines in Kotlin, you need three things:
- A job
- A dispatcher
- A scope
Job: Basically, a job is anything that can be canceled. Every coroutine has a job, and you can use the job to cancel the coroutine. Jobs can be arranged into parent-child hierarchies. Canceling a parent job immediately cancels all the job's children, which is a lot more convenient than canceling each coroutine manually.
Dispatcher: The dispatcher sends off coroutines to run on various threads. For example, Dispatchers.Main
runs tasks on the main thread, and Dispatchers.IO
offloads blocking I/O tasks to a shared pool of threads.
Scope: A coroutine's scope defines the context in which the coroutine runs. A scope combines information about a coroutine's job and dispatchers. Scopes keep track of coroutines. When you launch a coroutine, it's "in a scope," which means that you've indicated which scope will keep track of the coroutine.
Kotlin coroutines with Architecture components
A CoroutineScope
keeps track of all your coroutines and helps you to manage when your coroutines should run. It can also cancel all of the coroutines started within it. Each asynchronous operation or coroutine runs within a particular CoroutineScope
.
Architecture components provide first-class support for coroutines for logical scopes in your app. Architecture components define the following built-in scopes that you can use in your app. The built-in coroutine scopes are in the KTX extensions for each corresponding Architecture component. Be sure to add the appropriate dependencies when using these scopes.
ViewModelScope
: A ViewModelScope
is defined for each ViewModel
in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel
is cleared. In this codelab you will use ViewModelScope
to initiate the database operations.
Room and Dispatcher
When using the Room library to perform a database operation, Room uses a Dispatchers.IO
to perform the database operations in a background thread. You don't have to explicitly specify any Dispatchers
. Room does this for you.
6. Task: Collect and display the data
You want the user to be able to interact with the sleep data in the following ways:
- When the user taps the Start button, the app creates a new sleep night and stores the sleep night in the database.
- When the user taps the Stop button, the app updates the night with an end time.
- When the user taps the Clear button, the app erases the data in the database.
These database operations can take a long time, so they should run on a separate thread.
Step 1: Mark DAO functions as suspend functions
In SleepDatabaseDao.kt
, change the convenience methods to suspend functions.
- Open
database/SleepDatabaseDao.kt
, add suspend keyword to all the methods except forgetAllNights()
because Room already uses a background thread for that specific @Query which returns LiveData. The completeSleepDatabaseDao
class will look like this.
@Dao
interface SleepDatabaseDao {
@Insert
suspend fun insert(night: SleepNight)
@Update
suspend fun update(night: SleepNight)
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
suspend fun get(key: Long): SleepNight?
@Query("DELETE FROM daily_sleep_quality_table")
suspend fun clear()
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
suspend fun getTonight(): SleepNight?
@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
}
Step 2: Set up coroutines for database operations
When the Start button in the Sleep Tracker app is tapped, you want to call a function in the SleepTrackerViewModel
to create a new instance of SleepNight
and store the instance in the database.
Tapping any of the buttons triggers a database operation, such as creating or updating a SleepNight
. Since database operations can take a while, you use coroutines to implement click handlers for the app's buttons.
- Open the app-level
build.gradle
file. Under the dependencies section, you need the following dependencies, which were added for you.
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" // Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version"
- Open the
SleepTrackerViewModel.kt
file. - Define a variable called
tonight
to hold the current night. Make the variableMutableLiveData
, because you need to be able to observe the data and change it.
private var tonight = MutableLiveData<SleepNight?>()
- To initialize the
tonight
variable as soon as possible, create aninit
block below the definition oftonight
and callinitializeTonight()
. You defineinitializeTonight()
in the next step.
init {
initializeTonight()
}
- Below the
init
block, implementinitializeTonight()
. Use theviewModelScope.launch
to start a coroutine in theViewModelScope
. Inside the curly braces, get the value fortonight
from the database by callinggetTonightFromDatabase()
, and assign the value totonight.value
. You definegetTonightFromDatabase()
in the next step.
Notice the use of curly braces for launch
. They are defining a lambda expression, which is a function without a name. In this example, you are passing in a lambda to the launch
coroutine builder. This builder creates a coroutine and assigns the execution of that lambda to the corresponding dispatcher.
private fun initializeTonight() {
viewModelScope.launch {
tonight.value = getTonightFromDatabase()
}
}
- Implement
getTonightFromDatabase()
. Define it as aprivate suspend
function that returns a nullableSleepNight
, if there is no current startedSleepNight
. This leaves you with an error, because the function has to return something.
private suspend fun getTonightFromDatabase(): SleepNight? { }
- Inside the function body of
getTonightFromDatabase()
, gettonight
(the newest night) from the database. If the start and end times are not the same, meaning that the night has already been completed, returnnull
. Otherwise, return the night.
var night = database.getTonight()
if (night?.endTimeMilli != night?.startTimeMilli) {
night = null
}
return night
Your completed getTonightFromDatabase()
suspend
function should look like this. There should be no more errors.
private suspend fun getTonightFromDatabase(): SleepNight? {
var night = database.getTonight()
if (night?.endTimeMilli != night?.startTimeMilli) {
night = null
}
return night
}
Step 3: Add the click handler for the Start button
Now you can implement onStartTracking()
, the click handler for the Start button. You need to create a new SleepNight
, insert it into the database, and assign it to tonight
. The structure of onStartTracking()
is going to be similar to initializeTonight()
.
- In
SleepTrackerViewModel.kt
, start with the function definition foronStartTracking()
. You can put the click handlers belowgetTonightFromDatabase()
.
fun onStartTracking() {}
- Inside
onStartTracking()
, launch a coroutine in theviewModelScope
, because you need this result to continue and update the UI.
viewModelScope.launch {}
- Inside the coroutine launch, create a new
SleepNight
, which captures the current time as the start time.
val newNight = SleepNight()
- Still inside the coroutine launch, call
insert()
to insertnewNight
into the database. You will see an error, because you have not defined thisinsert()
suspend function yet. Note this is not the sameinsert()
as the method with the same name inSleepDatabaseDAO.kt
insert(newNight)
- Also inside the coroutine launch, update
tonight
.
tonight.value = getTonightFromDatabase()
- Below
onStartTracking()
, defineinsert()
as aprivate suspend
function that takes aSleepNight
as its argument.
private suspend fun insert(night: SleepNight) {}
- Within the
insert()
method, use the DAO to insert night into the database.
database.insert(night)
Note that a coroutine with Room uses Dispatchers.IO
, so this will not happen on the main thread.
- In the
fragment_sleep_tracker.xml
layout file, add the click handler foronStartTracking()
to thestart_button
using the magic of data binding that you set up earlier. The@{() ->
function notation creates a lambda function that takes no arguments and calls the click handler in thesleepTrackerViewModel
.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
- Build and run your app. Tap the Start button. This action creates data, but you cannot see anything yet. You fix that next.
Without Room
fun someWorkNeedsToBeDone {
viewModelScope.launch {
suspendFunction()
}
}
suspend fun suspendFunction() {
withContext(Dispatchers.IO) {
longrunningWork()
}
}
Using Room
// Using Room
fun someWorkNeedsToBeDone {
viewModelScope.launch {
suspendDAOFunction()
}
}
suspend fun suspendDAOFunction() {
// No need to specify the Dispatcher, Room uses Dispatchers.IO.
longrunningDatabaseWork()
}
Step 4: Display the data
In the SleepTrackerViewModel.kt
, the nights
variable references LiveData
because getAllNights()
in the DAO returns LiveData
.
It is a Room
feature that every time the data in the database changes, the LiveData
nights
is updated to show the latest data. You never need to explicitly set the LiveData
or update it. Room
updates the data to match the database.
However, if you display nights
in a text view, it will show the object reference. To see the contents of the object, transform the data into a formatted string. Use a Transformation
map that's executed every time nights
receives new data from the database.
- Open the
Util.kt
file and uncomment the code for the definition offormatNights()
and the associatedimport
statements. To uncomment code in Android Studio, select all the code marked with//
and pressCmd+/
orControl+/
. - Notice
formatNights()
returns a typeSpanned
, which is an HTML-formatted string. This is very convenient because Android's TextView has the ability to render basic HTML. - Open res > values > strings.xml. Notice the use of
CDATA
to format the string resources for displaying the sleep data. - Open SleepTrackerViewModel.kt. In the
SleepTrackerViewModel
class, define a variable callednights
. Get all the nights from the database and assign them to thenights
variable.
private val nights = database.getAllNights()
- Right below the definition of
nights
, add code to transformnights
into anightsString
. Use theformatNights()
function fromUtil.kt
.
Pass nights
into the map()
function from the Transformations
class. To get access to your string resources, define the mapping function as calling formatNights()
. Supply nights
and a Resources
object.
val nightsString = Transformations.map(nights) { nights ->
formatNights(nights, application.resources)
}
- Open the
fragment_sleep_tracker.xml
layout file. In theTextView
, in theandroid:text
property, you can now replace the resource string with a reference tonightsString
.
android:text="@{sleepTrackerViewModel.nightsString}"
- Rebuild your code and run the app. All your sleep data with start times should display now.
- Tap the Start button a few more times, and you see more data.
In the next step, you enable functionality for the Stop button.
Step 5: Add the click handler for the Stop button
Using the same pattern as in the previous step, implement the click handler for the Stop button in SleepTrackerViewModel.
- Add
onStopTracking()
to theViewModel
. Launch a coroutine in theviewModelScope
. If the end time hasn't been set yet, set theendTimeMilli
to the current system time and callupdate()
with the night data.
In Kotlin, the return@
label
syntax specifies the function from which this statement returns, among several nested functions.
fun onStopTracking() {
viewModelScope.launch {
val oldNight = tonight.value ?: return@launch
oldNight.endTimeMilli = System.currentTimeMillis()
update(oldNight)
}
}
- Implement
update()
using the same pattern as you used to implementinsert()
.
private suspend fun update(night: SleepNight) {
database.update(night)
}
- To connect the click handler to the UI, open the
fragment_sleep_tracker.xml
layout file and add the click handler to thestop_button
.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
- Build and run your app.
- Tap Start, then tap Stop. You see the start time, end time, sleep quality with no value, and time slept.
Step 6: Add the click handler for the Clear button
- Similarly, implement
onClear()
andclear()
.
fun onClear() {
viewModelScope.launch {
clear()
tonight.value = null
}
}
suspend fun clear() {
database.clear()
}
- To connect the click handler to the UI, open
fragment_sleep_tracker.xml
and add the click handler to theclear_button
.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
- Build and run your app.
- Tap Clear to get rid of all the data. Then tap Start and Stop to make new data.
7. Solution code
Android Studio project: TrackMySleepQualityCoroutines
8. Summary
- Use
ViewModel
,ViewModelFactory
, and data binding to set up the UI architecture for the app. - To keep the UI running smoothly, use coroutines for long-running tasks, such as all database operations.
- Coroutines are asynchronous and non-blocking. They use
suspend
functions to make asynchronous code sequential. - When a coroutine calls a function marked with
suspend
, instead of blocking until that function returns like a normal function call, it suspends execution until the result is ready. Then it resumes where it left off with the result. - The difference between blocking and suspending is that if a thread is blocked, no other work happens. If the thread is suspended, other work happens until the result is available.
To implement click handlers that trigger database operations, follow this pattern:
- Launch a coroutine that runs on the main or UI thread, because the result affects the UI.
- Call a suspend function to do the long-running work, so that you don't block the UI thread while waiting for the result.
- The long-running work has nothing to do with the UI, so switch to the I/O context. That way, the work can run in a thread pool that's optimized and set aside for these kinds of operations.
- Then call the long running function to do the work.
Use a Transformations
map to create a string from a LiveData
object every time the object changes.
9. Learn more
Udacity course:
Android Developer Documentation:
RoomDatabase
- Re-using layouts with <include/>
ViewModelProvider.Factory
SimpleDateFormat
HtmlCompat
Use Kotlin coroutines with Architecture components
Other documentation and articles:
- Factory pattern
- Coroutines codelab
- Coroutines, official documentation
- Coroutine context and dispatchers
Dispatchers
- Exceed the Android Speed Limit
Job
launch
- Returns and jumps in Kotlin
- CDATA stands for character data. CDATA means that the data in between these strings includes data that could be interpreted as XML markup, but should not be.
10. Homework
This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.
If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.
Answer these questions
Question 1
Which of the following is not a benefit of using coroutines?:
- They are non-blocking
- They run asynchronously.
- They can be run on a thread other than the main thread.
- They always make app runs faster.
- They can use exceptions.
- They can be written and read as linear code.
Question 2
Which of the following is not true for suspend functions.?
- An ordinary function annotated with the
suspend
keyword. - A function that can be called inside coroutines.
- While a suspend function is running, the calling thread is suspended.
- Suspend functions must always run in the background.
Question 3
Which of the following statements is NOT true?
- When execution is blocked, no other work can be executed on the blocked thread.
- When execution is suspended, the thread can do other work while waiting for the offloaded work to complete.
- Suspending is more efficient, because threads may not be waiting, doing nothing.
- Whether blocked or suspended, execution is still waiting for the result of the coroutine before continuing.
11. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.