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)