FLUTTER: Flutter Async Programming

Asynchronous programming is essential in Flutter for handling network requests, file I/O, and any operation that takes time to complete. Using Futures, async/await, and proper error handling ensures your app stays responsive while waiting for data.

Mobile apps constantly interact with the outside world — fetching data from APIs, reading from disk, waiting for sensor input. All of these operations take time. If your app blocks the main thread while waiting, the UI freezes and the user experience suffers. Asynchronous programming is the solution.


The Problem with Synchronous Code

In a synchronous model, your code executes one line at a time. If a network request takes two seconds, the entire app pauses for two seconds — buttons stop responding, animations freeze, and the app appears broken.

md
Synchronous Execution
----------------------
[fetch data] --> app frozen for 2 seconds --> [continue]
     |
     v
   BAD: UI blocks, app appears unresponsive

Flutter's main thread (also called the UI thread) is responsible for rendering 60 frames per second. Any blocking operation on this thread drops frames and causes visible stuttering.


The Solution: Futures and Async/Await

Dart solves this with two closely related concepts:

  • Futures represent a value that is not available yet but will be in the future
  • async/await provides syntactic sugar to write asynchronous code that reads like synchronous code

md
Asynchronous Execution
-----------------------
[start fetch] --> [UI continues running] --> [fetch completes] --> [handle result]
     |                                              |
     v                                              v
  Non-blocking                             Future resolves


Futures

A Future<T> is a placeholder for a value of type T that will be available at some point in the future. It can complete with either a value (success) or an error (failure).

dart
// A Future that completes with a String after 2 seconds
Future<String> fetchUsername() {
  return Future.delayed(
    const Duration(seconds: 2),
    () => 'amr_tarek',
  );
}

A Future has three states:

md
Future States
--------------
Uncompleted --> Completed with Value
            --> Completed with Error


async and await

The async keyword marks a function as asynchronous. The await keyword pauses execution within that function only — the rest of the app keeps running — until the awaited Future completes.

dart
// Without async/await (callback style — harder to read)
fetchUsername().then((name) {
  print('Username: $name');
}).catchError((error) {
  print('Error: $error');
});

// With async/await (much cleaner)
Future<void> loadUser() async {
  try {
    final name = await fetchUsername();
    print('Username: $name');
  } catch (error) {
    print('Error: $error');
  }
}

Any function marked async automatically returns a Future. You can await it from another async function.


Making API Calls

The most common use of async programming in Flutter is making HTTP requests to REST APIs.

API Call Steps

md
1. Identify the endpoint URL
2. Use the http package to send the request
3. Parse the JSON response with dart:convert
4. Update the UI with the new data
5. Handle errors gracefully

Complete API Call Example

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

// Model class
class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

// Async fetch function
Future<List<Post>> fetchPosts() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {'Content-Type': 'application/json'},
  ).timeout(const Duration(seconds: 10)); // Timeout after 10 seconds

  if (response.statusCode == 200) {
    final List<dynamic> jsonList = json.decode(response.body);
    return jsonList.map((json) => Post.fromJson(json)).toList();
  } else {
    throw Exception('Server error: ${response.statusCode}');
  }
}


FutureBuilder — Connecting Async Data to the UI

FutureBuilder is a Flutter widget that rebuilds itself based on the state of a Future. It handles the loading, success, and error states automatically.

dart
FutureBuilder<List<Post>>(
  future: fetchPosts(),
  builder: (context, snapshot) {
    // Still waiting for data
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }

    // An error occurred
    if (snapshot.hasError) {
      return Center(
        child: Text(
          'Something went wrong:\n${snapshot.error}',
          textAlign: TextAlign.center,
        ),
      );
    }

    // Data is available
    if (snapshot.hasData) {
      final posts = snapshot.data!;
      return ListView.builder(
        itemCount: posts.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(posts[index].title),
            subtitle: Text(posts[index].body),
          );
        },
      );
    }

    return const Center(child: Text('No data available'));
  },
)


Error Handling

Robust async code must handle failures gracefully.

try-catch

dart
Future<void> loadData() async {
  try {
    final data = await fetchPosts();
    setState(() => _posts = data);
  } on http.ClientException catch (e) {
    setState(() => _error = 'Network error: $e');
  } on FormatException catch (e) {
    setState(() => _error = 'Invalid data format: $e');
  } catch (e) {
    setState(() => _error = 'Unexpected error: $e');
  }
}

Timeouts

Prevent indefinitely hanging requests:

dart
Future<String> fetchWithTimeout() async {
  return await fetchData().timeout(
    const Duration(seconds: 5),
    onTimeout: () => throw TimeoutException('Request timed out'),
  );
}


Performance Best Practices

  • Debounce requests — delay API calls until the user stops typing to avoid overwhelming the server
  • Cache responses — store fetched data locally to avoid repeated network calls
  • Use HTTPS — never send data over insecure connections
  • Modularize async logic — keep network calls in dedicated service classes, not inside widgets
  • Avoid calling async in build() — initiate Future objects in initState(), not on every rebuild

dart
class _MyWidgetState extends State<MyWidget> {
  late Future<List<Post>> _futurePosts;

  @override
  void initState() {
    super.initState();
    // Fetch data once when the widget is created, not on every rebuild
    _futurePosts = fetchPosts();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Post>>(
      future: _futurePosts, // Use the stored Future, not a new one
      builder: (context, snapshot) { ... },
    );
  }
}