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)