flutter
/

Flutter FutureBuilder Widget Tutorial for Beginners

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter FutureBuilder Widget Tutorial for Beginners

What is FutureBuilder in Flutter?

FutureBuilder is a widget that builds itself based on the latest snapshot of interaction with a Future. It lets you handle asynchronous operations (like network calls, database queries, or file I/O) and update the UI automatically when the future completes – whether it succeeds with data or fails with an error.

Basic Usage

To use FutureBuilder, you provide a future and a builder function. The builder is called whenever the future's state changes. It receives a BuildContext and an AsyncSnapshot which contains the current state (loading, data, error).

DARTRead-only
1
FutureBuilder<String>(
  future: _fetchData(), // your async function
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else {
      return Text('Data: ${snapshot.data}');
    }
  },
)

Understanding AsyncSnapshot

The AsyncSnapshot object passed to the builder contains all the information about the asynchronous operation:

  • connectionState: One of ConnectionState.none (no future started), ConnectionState.waiting (future is in progress), ConnectionState.active (for streams), or ConnectionState.done (future completed).
  • hasData: Whether the snapshot contains non‑null data (i.e., the future completed successfully).
  • data: The value returned by the future (only available when hasData is true).
  • hasError: Whether the future completed with an error.
  • error: The error object (only available when hasError is true).

Building UI for Different States

A typical pattern is to handle three states: waiting (loading), error, and data. Here's a more detailed example:

DARTRead-only
1
FutureBuilder<User>(
  future: _fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Center(child: CircularProgressIndicator());
    }
    if (snapshot.hasError) {
      return Center(child: Text('Something went wrong: ${snapshot.error}'));
    }
    if (!snapshot.hasData) {
      return Center(child: Text('No user data'));
    }
    // Data is available
    final user = snapshot.data!;
    return Column(
      children: [
        Text('Name: ${user.name}'),
        Text('Email: ${user.email}'),
      ],
    );
  },
)

Handling Errors Gracefully

Always check snapshot.hasError before accessing snapshot.data. You can also provide a custom error widget or a retry button.

DARTRead-only
1
if (snapshot.hasError) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.error_outline, size: 48, color: Colors.red),
        SizedBox(height: 16),
        Text('Failed to load data'),
        ElevatedButton(
          onPressed: () => setState(() {}), // triggers rebuild with same future
          child: Text('Retry'),
        ),
      ],
    ),
  );
}

Important: Where to Create the Future

Never create the Future directly inside the build method, because that would create a new Future on every rebuild, causing the FutureBuilder to reset and re‑trigger the async operation continuously. Instead, store the Future in a state variable (or use a Future that doesn't change, like from a repository).

DARTRead-only
1
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late Future<String> _future;

  @override
  void initState() {
    super.initState();
    _future = _fetchData(); // created once
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _future, // safe to use here
      builder: (context, snapshot) { ... },
    );
  }
}

When the Future Changes

If you need to update the future (e.g., after a user action), assign a new Future to the variable and call setState. The FutureBuilder will automatically listen to the new Future.

DARTRead-only
1
void _refreshData() {
  setState(() {
    _future = _fetchData();
  });
}

Common Mistakes Beginners Make

  • Creating the future inside build: Leads to infinite rebuilds and wasted resources.
  • Not handling all connection states: For example, showing a loading indicator only when connectionState == ConnectionState.waiting is correct, but forgetting that ConnectionState.active might also be relevant for streams.
  • Accessing snapshot.data without checking hasData: Causes a null safety error if the future hasn't completed yet.
  • Ignoring errors: Always show a user‑friendly message or a retry option.
  • Assuming snapshot.data is non‑null when connectionState == ConnectionState.done: It might still be null if the future returned null. Check hasData instead.
  • Not using FutureBuilder for simple one‑time operations: Sometimes a Future with then and catchError inside initState plus setState is simpler, but FutureBuilder is declarative and handles state automatically.

Key Points to Remember

  • FutureBuilder rebuilds automatically when the future's state changes.
  • Always check snapshot.connectionState and snapshot.hasData / snapshot.hasError before using the data.
  • Store the future outside the build method (e.g., in initState) to avoid recreating it.
  • Use ConnectionState.waiting to show a loading indicator.
  • Handle errors gracefully with user feedback and optional retry.
  • For streams, use StreamBuilder which works similarly.

Common Interview Questions

  1. How does FutureBuilder work internally?
  2. What is the difference between ConnectionState.waiting and ConnectionState.done?
  3. Why should you avoid creating the Future inside the build method?
  4. How would you implement a retry button with FutureBuilder?
  5. Can FutureBuilder be used with a Future that never completes? What happens?
  6. How do you handle both data and error states in a FutureBuilder?
  7. What is the purpose of AsyncSnapshot?

Try it yourself

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('FutureBuilder Example')),
        body: Center(
          child: FutureExample(),
        ),
      ),
    );
  }
}

class FutureExample extends StatefulWidget {
  @override
  _FutureExampleState createState() => _FutureExampleState();
}

class _FutureExampleState extends State<FutureExample> {
  late Future<String> _future;

  @override
  void initState() {
    super.initState();
    _future = _fetchData();
  }

  Future<String> _fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    // Simulate occasional error
    if (DateTime.now().second % 3 == 0) {
      throw Exception('Network error');
    }
    return 'Hello from the future!';
  }

  void _refresh() {
    setState(() {
      _future = _fetchData();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        FutureBuilder<String>(
          future: _future,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator();
            } else if (snapshot.hasError) {
              return Column(
                children: [
                  Icon(Icons.error, color: Colors.red),
                  SizedBox(height: 8),
                  Text('Error: ${snapshot.error}'),
                ],
              );
            } else {
              return Text(
                snapshot.data ?? 'No data',
                style: TextStyle(fontSize: 18),
              );
            }
          },
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _refresh,
          child: Text('Retry'),
        ),
      ],
    );
  }
}

Test Your Knowledge

Q1
of 3

Which `ConnectionState` indicates that the future is still running?

A
ConnectionState.none
B
ConnectionState.waiting
C
ConnectionState.active
D
ConnectionState.done
Q2
of 3

What is the correct way to check if a FutureBuilder has successfully received data?

A
snapshot.data != null
B
snapshot.hasData
C
snapshot.connectionState == ConnectionState.done
D
snapshot.error == null
Q3
of 3

Why should you avoid creating the Future inside the build method of a FutureBuilder?

A
It causes a memory leak
B
It recreates the Future on every rebuild, leading to constant reloading
C
It makes the UI unresponsive
D
It prevents error handling

Previous

flutter appbar

Next

flutter streambuilder

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.