Flutter Async Await and Futures

Mobile apps fetch data from the internet, read files, and query databases. These operations take time. Dart uses Future, async, and await to handle time-consuming tasks without freezing the app.

The Problem — Blocking Code

Imagine a restaurant kitchen. If the chef waits at the oven doing nothing while the bread bakes, no other food gets cooked. A good chef starts the bread, then works on other dishes while waiting. Async programming works the same way.

  Blocking (bad):
  ─────────────────────────────────────────
  Start app → Fetch data from internet
  [App freezes] [App freezes] [App freezes]
  → Data arrives → App works again

  Non-blocking (good):
  ─────────────────────────────────────────
  Start app → Start data fetch (in background)
  → Show loading spinner → User can still scroll
  → Data arrives → Update UI automatically

What Is a Future

A Future is a promise of a value that will arrive later. It represents work that is happening in the background.

  Future<String> getUserName()

  Right now:  [?] (not ready yet)
  Later:      "Ravi Kumar" (resolved)  OR  Error (failed)
// A function that returns a Future
Future<String> fetchUsername() async {
  await Future.delayed(Duration(seconds: 2));  // Simulate delay
  return 'Ravi Kumar';
}

async and await

Mark a function with async to use await inside it. The await keyword pauses that function until the Future completes — but it does NOT freeze the rest of the app.

// Without async/await (harder to read)
fetchUsername().then((name) {
  print('Hello, $name');
}).catchError((error) {
  print('Error: $error');
});

// With async/await (cleaner)
Future<void> greetUser() async {
  try {
    String name = await fetchUsername();  // Waits here
    print('Hello, $name');
  } catch (error) {
    print('Error: $error');
  }
}

Future States

  Future lifecycle:
  ─────────────────────────────────────────
  Created → Pending → Completed (value) OR Error
                │
                │ During "Pending": show loading spinner
                │ On "Completed":   show data
                └ On "Error":       show error message

FutureBuilder — Futures in Flutter UI

FutureBuilder connects a Future to the widget tree. It automatically rebuilds the UI when the Future resolves.

FutureBuilder<String>(
  future: fetchUsername(),  // The async operation
  builder: (context, snapshot) {
    // snapshot.connectionState tells you the current state

    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();  // Loading
    }

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

    return Text('Hello, ${snapshot.data}!');  // Success
  },
)

FutureBuilder States Diagram

  FutureBuilder
  ─────────────────────────────────────────────
  ConnectionState.none     → Future not started
  ConnectionState.waiting  → Future running → show spinner
  ConnectionState.done     → Future finished:
    snapshot.hasError      → show error widget
    snapshot.hasData       → show data widget

Practical Example — Fetch User Profile

// Simulated API call (returns after 2 seconds)
Future<Map<String, String>> fetchProfile() async {
  await Future.delayed(Duration(seconds: 2));
  return {'name': 'Priya', 'email': 'priya@mail.com', 'city': 'Mumbai'};
}

class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: FutureBuilder<Map<String, String>>(
        future: fetchProfile(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Failed to load profile'));
          }
          final profile = snapshot.data!;
          return Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Name: ${profile['name']}', style: TextStyle(fontSize: 20)),
                Text('Email: ${profile['email']}'),
                Text('City: ${profile['city']}'),
              ],
            ),
          );
        },
      ),
    );
  }
}

Streams — Continuous Data

A Future delivers one value and finishes. A Stream delivers multiple values over time — like a live score update or a chat feed.

  Future:   [Delivery truck] → delivers one package → done
  Stream:   [Water pipe]     → flows continuously

  Future:   fetchWeather()    → one result
  Stream:   stockPriceStream  → new value every second
// Create a stream that emits a number every second
Stream<int> countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;  // yield sends a value into the stream
  }
}

// Use StreamBuilder to display stream data
StreamBuilder<int>(
  stream: countStream(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return Text('Waiting...');
    return Text('Count: ${snapshot.data}');
  },
)

Error Handling

Future<String> riskyOperation() async {
  try {
    String result = await someNetworkCall();
    return result;
  } catch (e) {
    print('Caught error: $e');
    return 'Default value';
  }
}

Key Rules

  • Add async to any function that uses await.
  • The return type becomes Future<T> automatically.
  • Always handle errors with try/catch or .catchError().
  • Use FutureBuilder to display async data in the UI.
  • Use StreamBuilder for data that changes over time.

Leave a Comment

Your email address will not be published. Required fields are marked *