Sunday, September 8, 2024

Tutorial Dart: Asynchronous Function

In our previous exploration of asynchronous programming, we laid the foundation for understanding its importance in maintaining application responsiveness, particularly when dealing with operations like network requests, file I/O, or heavy computations. Now, we'll delve deeper into the practical implementation of asynchronous code in Dart, exploring the core concepts of Futures, Callbacks, and Async/Await.

Why Asynchronous Code Matters

Asynchronous operations allow your program to continue working while waiting for another operation to complete. Here are some common scenarios where asynchronous operations shine:

  • Fetching data from a network: Downloading files, retrieving data from an API server, or making remote requests are all time-consuming processes that benefit from asynchronous handling.

  • Writing to a database: Persisting data to a database involves interactions with external systems that can introduce delays. Asynchronous operations help prevent the main thread from being blocked during these operations.

  • Reading data from a file: Reading large files can be resource-intensive. Asynchronous operations enable your program to process other tasks while the file reading happens in the background.

Unlocking the Power of Futures

A Future represents the eventual result of an asynchronous operation. Think of it as a promise that a value or an error will be delivered at some point in the future. Futures have two key states:

  • Completed: The asynchronous operation has finished, and the Future holds either a value (if successful) or an error.

  • Uncompleted: The asynchronous operation is still in progress.

Let's illustrate this with a simple example:

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() {
  return Future.delayed(
    const Duration(seconds: 2),
    () => 'Large Latte',
  );
}

void main() {
  print(createOrderMessage());
}
    
Output:
      Your order is: Instance of 'Future<String>'
    

Here's why the output is a Future<String> instead of the expected "Large Latte":

  1. fetchUserOrder() is an asynchronous function that returns a Future<String> representing the eventual order.

  2. createOrderMessage() calls fetchUserOrder() but doesn't wait for it to finish. It immediately returns a string containing the Future object.

  3. print() in main() doesn't wait for the Future to complete, so it prints the Future object itself.

So, how do we get the actual value from the Future? This is where the power of callbacks and the async/await keywords come in.

Navigating Futures with Callbacks

Dart provides a set of callback methods that enable you to interact with Futures once they complete. Let's explore these methods:

  1. then(): This method executes a callback function when the Future successfully completes with a value.

void main() {
  print('start fetching data');

  Future<String>.delayed(Duration(seconds: 2), () {
    return 'data are fetched';
  }).then((value) {
    print(value);
  });

  print('end fetching data');
}
    

Output:

start fetching data
end fetching data
data are fetched
    
  1. catchError(): This method allows you to handle any errors that occur during the execution of the Future.

void main() {
  print('start fetching data');
  
  Future<String>.delayed(Duration(seconds: 2), () {
    return 'data are fetched';
  }).then((value) {
    print(value);
  }).catchError((error) {
  print(error);
});

print('end fetching data');
}
    

Output:

start fetching data
end fetching data
data are fetched
    

Here's an example demonstrating catchError() handling an error:

Future<int> fetchNumber(bool succeed) {
  return Future.delayed(Duration(seconds: 2), () {
    if (succeed) {
      return 42;
    } else {
      throw Exception("Failed to fetch number");
    }
  });
}

void main() {
  fetchNumber(false).then((value) {
    print("The number is $value");
  }).catchError((error) {
  print("Error occurred: $error");
  });
  print("Waiting for the number...");
}
    

Output:

Waiting for the number...
Error occurred: Exception: Failed to fetch number
    

  1. whenComplete(): This method executes a callback function regardless of whether the Future completes successfully or with an error.

Future<int> fetchNumber(bool succeed) {
  return Future.delayed(Duration(seconds: 2), () {
    if (succeed){
      return 42;
    } else {
     throw Exception("Failed to fetch number");
    }
  });
}
  
void main() {
  fetchData(false).then((value) {
    print(value);
  }).catchError((error) {
    print("Error occurred: $error");
  }).whenComplete(() {
    print("Operation complete, cleaning up...");
  });

  print("Waiting for the operation to complete...");
}
    

Output:

Waiting for the operation to complete...
Error occurred: Exception: Failed to fetch number
Operation complete, cleaning up...
    

Embracing Async/Await: A More Declarative Approach

The async and await keywords offer a more declarative and intuitive way to handle asynchronous code. Here are the key principles:

  • async: Marks a function as asynchronous, allowing it to use await.

  • await: Pauses execution within an async function until the Future it's applied to completes, then returns the value or error.

Let's illustrate this with an example:

import 'dart:async';

  
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Data fetched successfully!';
}
  
Future<void> main() async {
  print('Fetching data...');

  String result = await fetchData();

  print(result);

  print('Done fetching data.');
}
    

Output:

Fetching data...
Data fetched successfully!
Done fetching data.
    

Explanation:

  1. fetchData() is an async function that simulates fetching data and introduces a 2-second delay.

  2. await before Future.delayed pauses execution until the delay completes.

  3. main() is also an async function because it uses await before fetchData().

  4. Execution pauses in main() until fetchData() returns its result.

Remember: async and await work in tandem to make asynchronous code look more like synchronous code, enhancing readability and maintainability.

Graceful Error Handling in Asynchronous Functions

Error handling is critical in asynchronous programming. You can use try-catch blocks within async functions to capture errors during await operations:

Future<void> printOrderMessage() async {
  try {
    print('Awaiting user order...');
    var order = await fetchUserOrder();
    print(order);
  } catch (err) {
    print('Caught error: $err');
  }
}

Future<String> fetchUserOrder() {

  // Imagine that this function is more complex.
  var str = Future.delayed( const Duration(seconds: 4), () {
    throw 'Cannot locate user order';
  });
  return str;
}

void main() async {
  await printOrderMessage();
}
    

Output:

Awaiting user order...
Caught error: Cannot locate user order
    

Alternatively, you can handle errors using callback methods:

Future<void> printOrderMessage() {
  print('Awaiting user order...');
  return fetchUserOrder().then((order) {
     print(order);
  }).catchError((err) {
    print('Caught error: $err');
  });
}

Future<String> fetchUserOrder() {
  
  // Imagine that this function is more complex.
  return Future.delayed(const Duration(seconds: 4), () {
    throw 'Cannot locate user order';
  });
}

void main() {
  printOrderMessage();
}
    

Output:

Awaiting user order...
Caught error: Cannot locate user order
    

Key Differences Between async/await and Callbacks

The choice between async/await and callbacks ultimately depends on your code's complexity, readability, and error-handling needs.

  • Readability: async/await syntax is generally considered more readable and intuitive, especially when dealing with complex asynchronous operations. Callback chains can become cumbersome with nested calls.

  • Error Handling: try-catch blocks within async functions offer a familiar and straightforward approach to error handling, similar to synchronous code. Callback methods like catchError() can sometimes be less intuitive.

Putting It All Together: Real-World Example

Let's demonstrate asynchronous programming in action by fetching data from an API server:

Using Callback Methods (then(), catchError()):

import 'package:http/http.dart' as http;
import 'dart:convert';

void fetchDataUsingCallbacks() {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
  
  http.get(url).then((response) {
    if (response.statusCode == 200) {
  
    var data = jsonDecode(response.body);
    print('Data fetched successfully: ${data['title']}');
    
  } else {
    print('Failed to load data');
  }

  }).catchError((error) {
    print('An error occurred: $error');
    
  }).whenComplete(() {
    print('Request complete.');
  });
}

  
void main() {
  fetchDataUsingCallbacks();
}
    

Output:

Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.
    

Error Case:

An error occurred: <error message> 
Request complete.
    

Using async/await with try(), catch():

import 'package:http/http.dart' as http;
import 'dart:convert';

  
Future<void> fetchDataUsingAsyncAwait() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');

  try {
    final response = await http.get(url);

    if (response.statusCode == 200) {

      var data = jsonDecode(response.body);
      print('Data fetched successfully: ${data['title']}');

    } else {
      print('Failed to load data');
    }
    
  } catch (error) {
    print('An error occurred: $error');
  } finally {
    print('Request complete.');
  }
}

  
void main() async {
  await fetchDataUsingAsyncAwait();
}
    

Output:

Data fetched successfully: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Request complete.
    

Error Case:

An error occurred: <error message> 
Request complete.
    

Conclusion

This deep dive into Futures, Callbacks, and Async/Await has provided you with the tools to confidently implement asynchronous programming in your Dart applications. By embracing these concepts, you can build applications that remain responsive and efficient even when dealing with time-consuming operations, enhancing user experience and application performance. As you continue your Dart journey, remember that mastering asynchronous programming is essential for building robust and scalable applications.

0 comments:

Post a Comment