## Learning Objectives

By the end of this chapter, you will be able to:

- Understand asynchronous programming concepts and why they matter in Flutter
- Use Future and async/await for single asynchronous operations
- Work with Streams for continuous data flows
- Handle errors in asynchronous code effectively
- Use Isolates for CPU-intensive tasks
- Apply best practices for async operations in Flutter apps

1

## Why Asynchronous Programming?

### The Problem

Mobile apps need to perform operations that take time (network requests, file I/O, database queries). If these operations block the UI thread, the app becomes unresponsive and freezes.

### The Solution

Asynchronous programming allows time-consuming operations to run in the background while the UI remains responsive. Flutter uses Dart's async/await syntax and Future/Stream APIs for this.

Key principle: Never block the UI thread. All I/O operations should be asynchronous.

2

## Futures and async/await

### What is a Future?

A `Future` represents a value that will be available at some point in the future. It's used for single asynchronous operations that return one value.

### Basic Future Example

    Future fetchData() async {
      // Simulate network delay
      await Future.delayed(Duration(seconds: 2));
      return 'Data loaded';
    }

    // Usage with async/await
    void loadData() async {
      final data = await fetchData();
      print(data);  // Prints: Data loaded
    }

### async/await Syntax

- Mark functions that use `await` with `async`
- `await` pauses execution until the Future completes
- Async functions return a `Future` automatically
- Use `await` to get the actual value from a Future

### Multiple Async Operations

    // Sequential (one after another)
    Future loadSequential() async {
      final user = await fetchUser();
      final posts = await fetchPosts(user.id);
      print('Loaded ${posts.length} posts');
    }

    // Parallel (at the same time)
    Future loadParallel() async {
      final results = await Future.wait([
        fetchUser(),
        fetchPosts(),
        fetchComments(),
      ]);
      // All complete at the same time
    }

3

## Error Handling in Async Code

### try-catch with async/await

Use try-catch blocks to handle errors in async functions:

### Error Handling Example

    Future loadDataSafely() async {
      try {
        final data = await fetchData();
        print('Success: $data');
      } on TimeoutException {
        print('Request timed out');
      } on HttpException catch (e) {
        print('HTTP error: $e');
      } catch (e) {
        print('Unexpected error: $e');
      }
    }

### Future Error Handling

You can also use `then` and `catchError` with Futures:

### then/catchError Pattern

    fetchData()
      .then((data) {
        print('Success: $data');
      })
      .catchError((error) {
        print('Error: $error');
      });

4

## Streams for Continuous Data

### What is a Stream?

A `Stream` provides a sequence of asynchronous events over time. Use streams for continuous data flows like user input, network responses, or file reading.

### Basic Stream Example

    Stream countStream() async* {
      for (int i = 1; i <= 5; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i;  // Emit value
      }
    }

    // Listening to a stream
    void listenToStream() {
      countStream().listen(
        (value) => print('Received: $value'),
        onError: (error) => print('Error: $error'),
        onDone: () => print('Stream completed'),
      );
    }

### StreamBuilder Widget

Flutter's `StreamBuilder` widget rebuilds UI when stream emits new data:

### StreamBuilder Example

    StreamBuilder(
      stream: countStream(),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        return Text('Count: ${snapshot.data}');
      },
    )

5

## Common Stream Operations

### Stream Transformations

- `map()` — Transform each event
- `where()` — Filter events
- `take()` — Take first N events
- `skip()` — Skip first N events
- `distinct()` — Remove duplicates
- `debounceTime()` — Wait for pause in events

### Stream Transformations Example

    // Transform stream
    final numbers = Stream.fromIterable([1, 2, 3, 4, 5]);
    final doubled = numbers.map((n) => n * 2);
    final evens = numbers.where((n) => n % 2 == 0);

    // Combine streams
    final stream1 = Stream.value(1);
    final stream2 = Stream.value(2);
    final combined = StreamZip([stream1, stream2]);

6

## Isolates for Heavy Computation

### What are Isolates?

Isolates are separate execution threads that run in parallel. They don't share memory, so they're perfect for CPU-intensive tasks that would block the UI thread.

### When to Use Isolates

- Heavy computations (image processing, large data parsing)
- Complex calculations that take significant time
- JSON parsing of very large files
- Any CPU-bound work that could freeze the UI

### Using compute() Function

    // Heavy computation function (must be top-level or static)
    int calculateSum(List numbers) {
      int sum = 0;
      for (int num in numbers) {
        sum += num * num;  // Simulate heavy work
      }
      return sum;
    }

    // Use compute() to run in isolate
    Future processData() async {
      final numbers = List.generate(1000000, (i) => i);
      final result = await compute(calculateSum, numbers);
      print('Sum: $result');  // UI remains responsive
    }

### compute() Requirements

- The function must be top-level or static
- Parameters must be serializable (primitive types, lists, maps)
- Return value must be serializable
- No access to closures or instance variables

7

## FutureBuilder and Async Operations in UI

### FutureBuilder Widget

Use `FutureBuilder` to build UI based on Future completion:

### FutureBuilder Example

    FutureBuilder>(
      future: fetchUsers(),
      builder: (context, snapshot) {
        // Check connection state
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        // Check for errors
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }

        // Check if data exists
        if (!snapshot.hasData) {
          return Text('No data');
        }

        // Build UI with data
        final users = snapshot.data!;
        return ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) {
            return ListTile(title: Text(users[index].name));
          },
        );
      },
    )

### ConnectionState Values

- `ConnectionState.none` — No connection yet
- `ConnectionState.waiting` — Waiting for data
- `ConnectionState.active` — Stream is active (for StreamBuilder)
- `ConnectionState.done` — Operation completed

8

## Best Practices

### Async Programming Best Practices

- Always handle errors in async operations
- Use `async`/`await` instead of `then`/`catchError` for readability
- Don't forget to check `mounted` before setState in async callbacks
- Use `Future.timeout()` to prevent indefinite waiting
- Cancel streams and futures when widgets are disposed
- Use `compute()` for CPU-intensive tasks
- Avoid creating unnecessary isolates for simple operations
- Prefer `StreamBuilder` and `FutureBuilder` for reactive UI updates

9

## Exercises

### 1. Async Data Loading

Create a screen that fetches user data from an API (use a mock function). Use FutureBuilder to display loading, error, and success states. Add a retry button for error cases.

### 2. Stream-based Timer

Build a countdown timer using a Stream that emits values every second. Use StreamBuilder to display the remaining time. Add start, pause, and reset functionality.

### 3. Heavy Computation with Isolates

Create a function that calculates the sum of squares for a large list (1 million numbers). Use compute() to run it in an isolate. Show a loading indicator while computing and display the result when done. Compare the UI responsiveness with and without isolates.