Kotlin Suspending Functions
When working with Kotlin Coroutines, suspending functions are one of the most important concepts to understand. They form the foundation of Kotlin's approach to asynchronous programming, allowing you to write code that is both efficient and easy to read.
What are Suspending Functions?
A suspending function is a special type of function that can pause its execution at certain points without blocking the thread it's running on. This capability enables non-blocking asynchronous programming in Kotlin.
Think of suspending functions as regular functions with an additional ability: they can "suspend" execution until a result is ready, and then continue where they left off.
Key Characteristics
- Marked with the
suspend
keyword - Can call other suspending functions
- Can only be called from coroutines or other suspending functions
- Do not block threads while waiting for operations to complete
Basic Syntax
Here's the basic syntax for defining a suspending function:
suspend fun myFunction() {
// Function body
}
The suspend
modifier is what makes this function special - it tells the compiler that this function may suspend execution without blocking the thread.
How Suspending Functions Work
When a suspending function is called, it can perform some operations and then "suspend" or pause execution. The thread that was running the function is free to do other work. When the suspending point completes its work, the function resumes where it left off, potentially on a different thread.
Let's see a basic example:
import kotlinx.coroutines.*
// A simple suspending function
suspend fun fetchData(): String {
delay(1000) // Suspends the coroutine for 1 second (non-blocking)
return "Data loaded"
}
fun main() = runBlocking {
println("Starting to fetch data...")
val data = fetchData() // Execution is suspended here without blocking the thread
println("Data received: $data")
}
Output:
Starting to fetch data...
Data received: Data loaded
In the above example:
fetchData()
is a suspending function that simulates a network request with a 1-second delay- When
fetchData()
is called, the coroutine is suspended at thedelay()
call - After 1 second, execution resumes and the function returns "Data loaded"
Suspension Points
A suspension point is any call to another suspending function within a suspending function. At these points, the function may yield its thread and suspend execution.
Common suspension points include:
delay()
: Suspends the coroutine for a specified timeawait()
: Suspends until an asynchronous operation completeswithContext()
: Switches to a different coroutine context and suspends- Calls to other suspending functions
Calling Suspending Functions
Suspending functions can only be called from:
- Another suspending function
- Within a coroutine builder like
launch
,async
,runBlocking
, etc.
They cannot be called from regular functions.
// Regular function - CANNOT call suspending functions directly
fun regularFunction() {
// fetchData() // Error: Suspending function called from a non-suspending context
}
// Suspending function - CAN call other suspending functions
suspend fun anotherSuspendingFunction() {
val data = fetchData() // This is allowed
}
// Using a coroutine builder to call a suspending function
fun usingCoroutineBuilder() {
GlobalScope.launch {
val data = fetchData() // This is allowed inside a coroutine
}
}
Practical Examples
Example 1: Sequential API Calls
One common use case for suspending functions is making sequential API calls:
suspend fun getUserProfile(userId: String): Profile {
val userDetails = fetchUserDetails(userId) // First API call
val userPreferences = fetchUserPreferences(userId) // Second API call
return Profile(userDetails, userPreferences)
}
suspend fun fetchUserDetails(userId: String): UserDetails {
delay(1000) // Simulate network request
return UserDetails(userId, "John Doe", "[email protected]")
}
suspend fun fetchUserPreferences(userId: String): UserPreferences {
delay(800) // Simulate network request
return UserPreferences(theme = "Dark", language = "English")
}
fun main() = runBlocking {
val profile = getUserProfile("user123")
println("User: ${profile.userDetails.name}")
println("Preferences: ${profile.userPreferences.theme} theme")
}
Output:
User: John Doe
Preferences: Dark theme
This code looks synchronous but actually performs asynchronous operations without blocking the thread.
Example 2: Parallel API Calls with async
We can improve the previous example by executing the API calls in parallel:
suspend fun getUserProfileConcurrently(userId: String): Profile = coroutineScope {
val userDetailsDeferred = async { fetchUserDetails(userId) }
val userPreferencesDeferred = async { fetchUserPreferences(userId) }
// Both requests are running concurrently, but we wait for both to complete
val userDetails = userDetailsDeferred.await()
val userPreferences = userPreferencesDeferred.await()
Profile(userDetails, userPreferences)
}
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val profile = getUserProfileConcurrently("user123")
val endTime = System.currentTimeMillis()
println("User: ${profile.userDetails.name}")
println("Preferences: ${profile.userPreferences.theme} theme")
println("Time taken: ${endTime - startTime} ms") // Will be ~1000ms instead of ~1800ms
}
Output:
User: John Doe
Preferences: Dark theme
Time taken: 1020 ms
Example 3: Error Handling in Suspending Functions
Suspending functions can use traditional try-catch blocks for error handling:
suspend fun fetchUserDataSafely(userId: String): UserData {
return try {
val details = fetchUserDetails(userId)
val preferences = fetchUserPreferences(userId)
UserData(details, preferences, success = true)
} catch (e: Exception) {
println("Error fetching user data: ${e.message}")
UserData(null, null, success = false)
}
}
fun main() = runBlocking {
val userData = fetchUserDataSafely("invalidUser")
if (userData.success) {
println("User data loaded successfully")
} else {
println("Failed to load user data")
}
}
Building Your Own Suspending Functions
You can create your own suspending functions to wrap callback-based APIs or blocking operations:
Example: Converting a Callback-based API to a Suspending Function
// Traditional callback-based approach
fun fetchUserDataWithCallback(userId: String, callback: (UserData?, Error?) -> Unit) {
// Implementation of a traditional callback API
// ...
}
// Converted to a suspending function using suspendCoroutine
suspend fun fetchUserDataSuspend(userId: String): UserData = suspendCoroutine { continuation ->
fetchUserDataWithCallback(userId) { userData, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(userData!!)
}
}
}
// Now you can use it like a regular suspending function
fun main() = runBlocking {
try {
val userData = fetchUserDataSuspend("user123")
println("User data: $userData")
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
Example: Making Blocking Calls Non-blocking
suspend fun readFileSuspending(filePath: String): String = withContext(Dispatchers.IO) {
// This code runs in an IO-optimized thread pool
File(filePath).readText() // Blocking I/O operation
}
fun main() = runBlocking {
val fileContent = readFileSuspending("config.txt")
println("File content: $fileContent")
}
Best Practices for Suspending Functions
- Keep functions focused: Each suspending function should perform a single task.
- Document suspension points: Make it clear where functions may suspend.
- Handle errors properly: Use try-catch blocks for error handling.
- Choose the right context: Use appropriate dispatchers for CPU-intensive or I/O-bound operations.
- Avoid blocking operations: Don't use
Thread.sleep()
or blocking I/O in suspending functions.
Common Pitfalls to Avoid
- Calling suspending functions from regular functions: Always call suspending functions from a coroutine or another suspending function.
- Blocking threads: Avoid using blocking calls inside suspending functions.
- Not handling exceptions: Always handle exceptions in suspending functions.
- Leaking coroutines: Always ensure coroutines are properly cancelled when no longer needed.
Summary
Suspending functions are a powerful feature of Kotlin's coroutines system that enable you to write asynchronous code in a sequential, readable manner. They allow your code to pause execution without blocking threads, making your applications more efficient and responsive.
Key points to remember:
- Suspending functions are marked with the
suspend
modifier - They can only be called from other suspending functions or within coroutine builders
- They enable non-blocking asynchronous code that looks synchronous
- They form the foundation of Kotlin's approach to concurrency
Exercises
- Create a suspending function that simulates fetching weather data and includes a delay.
- Convert a callback-based API of your choice to use suspending functions.
- Write a function that performs three API calls concurrently and combines their results.
- Implement proper error handling for a chain of suspending function calls.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)