In previous lessons, almost all the code we wrote executed synchronously – code runs from top to bottom, with each line completing before the next one starts. But in real-world development, many operations (like network requests or file I/O) need to wait for external responses. Handling these with synchronous code would cause our programs to "freeze." Today we'll learn about asynchronous programming – a core technology for solving these problems, which is especially crucial in UI applications (like Flutter apps).
I. Synchronous vs Asynchronous: Why UI Programs Need Can't Live Without Asynchronous Programming
1. Limitations of Synchronous Execution
Synchronous code flows in a straight line, each step must wait for the previous one to complete:
void main() {
print("Starting execution");
print("Performing time-consuming operation...");
// Simulate a 3-second operation (like a network request)
for (int i = 0; i < 1000000000; i++) {}
print("Operation completed");
print("Continuing with other tasks");
}
// Output order:
// Starting execution
// Performing time-consuming operation...
// (3-second wait)
// Operation completed
// Continuing with other tasks
This pattern causes serious problems with time-consuming operations (like network requests or large data processing):
- The entire program becomes "blocked" and can't respond to user actions (like button clicks or screen swipes)
- The UI freezes, creating a poor "unresponsive" user experience
2. Advantages of Asynchronous Execution
The core of asynchronous code is: Time-consuming operations run in the "background" while the main thread continues processing other tasks, with results handled once the operation completes.
A real-life analogy:
- Synchronous: Staring at a kettle while waiting for water to boil, doing nothing else until it's ready.
- Asynchronous: Preparing tea leaves and cups while waiting for water to boil, then returning when it's ready.
In UI applications, asynchronous programming is essential:
- Keeps the UI thread unblocked, ensuring it can always respond to user actions
- Improves program efficiency by allowing multiple tasks to be processed "in parallel"
II. Future: The "Placeholder" for Asynchronous Operations
In Dart, a Future represents an "operation that will complete in the future." It's a placeholder for an asynchronous operation – we don't know the result when we create it, but we know we'll get one eventually (either a success or failure).
1. Three States of a Future
- Pending: The asynchronous operation is in progress, no result yet
- Completed with value: The asynchronous operation succeeded, returning a result
- Completed with error: The asynchronous operation failed, returning an error
2. Creating Futures and Handling Results
Create asynchronous operations with the Future constructor, and handle success with then, errors with catchError:
void main() {
print("Starting main thread tasks");
// Create a Future (asynchronous operation)
Future<String> fetchData() {
// Simulate network request returning after 2 seconds
return Future.delayed(Duration(seconds: 2), () {
// Simulate success scenario
return "Fetched data: Dart Asynchronous Programming";
// Simulate failure scenario (comment out above line and uncomment below)
// throw Exception("Network error: failed to fetch data");
});
}
// Call the async function to get a Future object
Future<String> future = fetchData();
// Register callback: executes when Future completes successfully
future
.then((data) {
print("Async operation succeeded: $data");
})
.catchError((error) {
// Register callback: executes when Future fails
print("Async operation failed: ${error.toString()}");
})
.whenComplete(() {
// Register callback: always executes whether success or failure
print("Async operation finished (success or failure)");
});
print("Main thread continues with other tasks");
}
// Output order (success scenario):
// Starting main thread tasks
// Main thread continues with other tasks
// (2-second wait)
// Async operation succeeded: Fetched data: Dart Asynchronous Programming
// Async operation finished (success or failure)
Key characteristics:
- After calling fetchData(), the main thread doesn't wait but immediately executes the next print
- The callback in then executes only after the async operation completes after 2 seconds
- catchError captures errors thrown in the async operation
- whenComplete is similar to "finally" and executes regardless of success or failure
3. Future.value and Future.error
Quickly create "already completed" Futures:
void main() {
// Create a successfully completed Future directly
Future.value("Direct success result").then((data) {
print(data); // Output: Direct success result
});
// Create a failed Future directly
Future.error(Exception("Direct error")).catchError((error) {
print(error); // Output: Exception: Direct error
});
}
III. async/await: Writing Asynchronous Code with Synchronous Style (Key Focus)
While then chaining can handle asynchronous operations, multiple levels of nesting lead to "callback hell" (bloated, hard-to-maintain code). Dart provides async/await syntactic sugar that lets us write asynchronous logic with synchronous-style code.
1. Basic Usage
- async: Modifies a function, indicating it's asynchronous, with its return value automatically wrapped in a Future
- await: Can only be used inside async functions, waits for a Future to complete and retrieves its result
void main() {
print("Starting main thread tasks");
// Call async function (main thread continues without waiting)
fetchAndPrintData();
print("Main thread continues with other tasks");
}
// Modified with async, indicating this is an asynchronous function
Future<void> fetchAndPrintData() async {
try {
print("Starting async data fetch");
// Use await to wait for Future completion and get result (synchronous-style)
String data = await fetchData(); // Waits for fetchData() to complete
// The await above "pauses" the function until the Future completes
print("Async operation succeeded: $data");
} catch (error) {
// Catches errors in async operations (replaces catchError)
print("Async operation failed: ${error.toString()}");
} finally {
// Executes regardless of success or failure (replaces whenComplete)
print("Async operation finished (success or failure)");
}
}
// Async function simulating network request
Future<String> fetchData() {
return Future.delayed(Duration(seconds: 2), () {
return "Fetched data: async/await is convenient";
// Simulate failure: throw Exception("Network error");
});
}
// Output order:
// Starting main thread tasks
// Main thread continues with other tasks
// Starting async data fetch
// (2-second wait)
// Async operation succeeded: Fetched data: async/await is convenient
// Async operation finished (success or failure)
2. Why Do async Functions Return Futures?
- Functions modified with async have their return values automatically wrapped in a Future
- If a function returns int, its actual return type is Future
- If a function has no return value, its actual return type is Future
// Returns Future<int>
Future<int> calculate() async {
await Future.delayed(Duration(seconds: 1));
return 100; // Automatically wrapped as Future.value(100)
}
void main() async {
int result = await calculate(); // Use await to get the result
print(result); // Output: 100
}
3. Handling Multiple Asynchronous Operations
async/await simplifies sequential execution of multiple asynchronous operations:
// Simulate three async operations
Future<String> fetchUser() =>
Future.delayed(Duration(seconds: 1), () => "User information");
Future<String> fetchOrders() =>
Future.delayed(Duration(seconds: 1), () => "Order list");
Future<String> fetchRecommendations() =>
Future.delayed(Duration(seconds: 1), () => "Recommended products");
// Execute multiple async operations sequentially
Future<void> fetchAllData() async {
print("Starting to fetch all data...");
// Execute sequentially, total time ~3 seconds
String user = await fetchUser();
print("Fetched: $user");
String orders = await fetchOrders();
print("Fetched: $orders");
String recommendations = await fetchRecommendations();
print("Fetched: $recommendations");
print("All data fetching completed");
}
void main() {
fetchAllData();
}
For independent async operations, execute them in parallel (with Future.wait):
Future<void> fetchAllDataParallel() async {
print("Starting parallel fetch of all data...");
// Execute in parallel, total time ~1 second (takes longest single operation time)
List<Future<String>> futures = [
fetchUser(),
fetchOrders(),
fetchRecommendations(),
];
// Wait for all Futures to complete, returns list of results
List<String> results = await Future.wait(futures);
for (String result in results) {
print("Fetched: $result");
}
print("All data fetching completed");
}
IV. Common Asynchronous Programming Pitfalls
1. Forgetting to Use await
Future<int> getNumber() async => 42;
void main() async {
// Error: Forgot to use await, getting Future instead of result
var result = getNumber();
print(result); // Output: Instance of 'Future<int>'
// Correct: Use await to get result
var correctResult = await getNumber();
print(correctResult); // Output: 42
}
2. Using await in Non-async Functions
// Error: await can only be used in async functions
void badFunction() {
// await Future.delayed(Duration(seconds: 1)); // Compile error
}
// Correct: Modify function with async
void goodFunction() async {
await Future.delayed(Duration(seconds: 1)); // Correct
}
3. Unhandled Exceptions Causing Crashes
Future<void> riskyOperation() async {
throw Exception("Unexpected error"); // Throw exception
}
void main() async {
// Error: Unhandled exception will crash the program
// await riskyOperation();
// Correct: Handle exception with try-catch
try {
await riskyOperation();
} catch (e) {
print("Caught exception: $e"); // Safe handling
}
}
V. Practical Application Scenarios
Asynchronous programming is everywhere in real-world development:
Network requests:
Future<User> fetchUserInfo(String userId) async {
final response = await http.get(
Uri.parse("https://api.example.com/users/$userId"),
);
if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception("Failed to load user");
}
}
Local storage operations:
Future<void> saveData(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
}
Delayed execution:
Future<void> showSplashScreen() async {
print("Showing splash screen");
await Future.delayed(Duration(seconds: 3)); // Wait 3 seconds
print("Closing splash screen, entering home page");
}
Top comments (0)