Ahmet Balaman LogoAhmet Balaman

Flutter: Asynchronous Listing with FutureBuilder

personAhmet Balaman
calendar_today
FlutterFutureBuilderAsyncAwaitWidgetAPI

FutureBuilder is a special structure designed in Flutter to use asynchronous operations within widgets. Since widgets don't directly have async capability, FutureBuilder is required to use asynchronous functions in the UI.

Why FutureBuilder?

  • When using functions with async capability, we use await to make it work only until it completes its task
  • However, to use await, we need to be inside a function with async capability
  • When we want to use an async function inside a widget, it needs to have async capability
  • Widgets don't have this capability!
  • FutureBuilder structure is required to use async capability within widgets

Live Demo

You can try this widget in the interactive example below:

💡 If the example above doesn't load, click DartPad to run it in a new tab.

Basic Usage

FutureBuilder<String>(
  future: fetchData(), // async function
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text('Data: ${snapshot.data}');
    }
    return Text('No data found');
  },
)

// Async function
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Hello Flutter!';
}

Important Properties

Property Description
future The Future object to wait for
builder Function that builds the UI
initialData Initial data value

ConnectionState States

State Description
none Future not yet assigned
waiting Future is running, waiting
active Active data stream for Stream
done Future completed

FutureBuilder with ListView

Fetching data from API and creating a list:

class MyListPage extends StatelessWidget {
  Future<List<String>> fetchItems() async {
    // API call simulation
    await Future.delayed(Duration(seconds: 2));
    return ['Flutter', 'Dart', 'Firebase', 'Android', 'iOS'];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('List')),
      body: FutureBuilder<List<String>>(
        future: fetchItems(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          
          if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('No data found'));
          }

          final items = snapshot.data!;
          return ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return ListTile(
                leading: CircleAvatar(child: Text('${index + 1}')),
                title: Text(items[index]),
              );
            },
          );
        },
      ),
    );
  }
}

FutureBuilder with GridView

We can use the same approach with GridView.builder:

class MyGridPage extends StatelessWidget {
  Future<List<Map<String, dynamic>>> fetchProducts() async {
    await Future.delayed(Duration(seconds: 2));
    return [
      {'name': 'Product 1', 'price': 99, 'color': Colors.red},
      {'name': 'Product 2', 'price': 149, 'color': Colors.blue},
      {'name': 'Product 3', 'price': 199, 'color': Colors.green},
      {'name': 'Product 4', 'price': 249, 'color': Colors.orange},
      {'name': 'Product 5', 'price': 79, 'color': Colors.purple},
      {'name': 'Product 6', 'price': 129, 'color': Colors.teal},
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Products')),
      body: FutureBuilder<List<Map<String, dynamic>>>(
        future: fetchProducts(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          
          if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('No products found'));
          }

          final products = snapshot.data!;
          return GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              mainAxisSpacing: 8,
              crossAxisSpacing: 8,
            ),
            padding: EdgeInsets.all(8),
            itemCount: products.length,
            itemBuilder: (context, index) {
              final product = products[index];
              return Card(
                color: product['color'],
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        product['name'],
                        style: TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      SizedBox(height: 8),
                      Text(
                        '\$${product['price']}',
                        style: TextStyle(color: Colors.white),
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Fetching Real Data from API

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

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}

class UsersPage extends StatelessWidget {
  Future<List<User>> fetchUsers() async {
    final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/users'),
    );
    
    if (response.statusCode == 200) {
      final List<dynamic> data = json.decode(response.body);
      return data.map((json) => User.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load users');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: FutureBuilder<List<User>>(
        future: fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error, size: 64, color: Colors.red),
                  SizedBox(height: 16),
                  Text('Error: ${snapshot.error}'),
                ],
              ),
            );
          }
          
          final users = snapshot.data!;
          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return Card(
                margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
                child: ListTile(
                  leading: CircleAvatar(
                    child: Text(user.name[0]),
                  ),
                  title: Text(user.name),
                  subtitle: Text(user.email),
                  trailing: Icon(Icons.chevron_right),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

FutureBuilder Best Practices

1. Store Future in State

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  late Future<List<String>> _futureData;

  @override
  void initState() {
    super.initState();
    _futureData = fetchData(); // Create Future once
  }

  Future<List<String>> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return ['Data 1', 'Data 2', 'Data 3'];
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<String>>(
      future: _futureData, // Use the same Future
      builder: (context, snapshot) {
        // ...
      },
    );
  }
}

2. Refresh Feature

class _MyPageState extends State<MyPage> {
  late Future<List<String>> _futureData;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  void _loadData() {
    setState(() {
      _futureData = fetchData();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Data'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _loadData, // Refresh
          ),
        ],
      ),
      body: FutureBuilder<List<String>>(
        future: _futureData,
        builder: (context, snapshot) {
          // ...
        },
      ),
    );
  }
}

Summary

  • FutureBuilder: For using async in widgets
  • ConnectionState: Waiting, error, completion states
  • snapshot.hasData: Data check
  • snapshot.hasError: Error check
  • ListView.builder + FutureBuilder: Dynamic async list
  • GridView.builder + FutureBuilder: Dynamic async grid

FutureBuilder is an indispensable structure for using asynchronous operations like API calls and database operations in Flutter UI.

Comments