DEV Community

vmodal_ai
vmodal_ai

Posted on

Understanding FutureBuilder and StreamBuilder in Flutter: A Beginner-Friendly Guide

If you've started building Flutter apps, you've probably seen widgets like FutureBuilder and StreamBuilder. At first, they can seem confusing, but they're essential for working with data that doesn't arrive immediately, such as API responses, databases, or real-time updates.

In this tutorial, you'll learn what these widgets do, when to use each one, and how to build simple examples step by step.


What Are FutureBuilder and StreamBuilder?

Both widgets help Flutter build the UI based on asynchronous data.

The difference is simple:

  • FutureBuilder waits for one result and then stops.
  • StreamBuilder listens for multiple results over time.

Think of it like this:

Future
Request → Waiting → One Result → Finished

Stream
Listening → Result 1 → Result 2 → Result 3 → ...
Enter fullscreen mode Exit fullscreen mode

When Should You Use FutureBuilder?

Use FutureBuilder when you only need data once.

Examples include:

  • Loading user information
  • Fetching products from an API
  • Reading a file
  • Loading settings from local storage

When Should You Use StreamBuilder?

Use StreamBuilder when data changes continuously.

Examples include:

  • Chat messages
  • Live stock prices
  • GPS location updates
  • Firebase Firestore
  • Sensor data

Part 1: Understanding FutureBuilder

Step 1: Create a New Flutter Project

flutter create future_builder_demo
Enter fullscreen mode Exit fullscreen mode

Open the project in your favorite editor.


Step 2: Create a Function That Returns Data

Let's pretend we're loading user data from the internet.

Future<String> fetchUser() async {
  await Future.delayed(
    const Duration(seconds: 3),
  );

  return "Welcome John!";
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • Future.delayed() simulates a network request.
  • The function returns data after 3 seconds.

Step 3: Display the Result with FutureBuilder

FutureBuilder<String>(
  future: fetchUser(),
  builder: (context, snapshot) {

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

    if (snapshot.hasError) {
      return Text(snapshot.error.toString());
    }

    return Text(snapshot.data!);
  },
)
Enter fullscreen mode Exit fullscreen mode

Let's understand each part.

future

future: fetchUser()
Enter fullscreen mode Exit fullscreen mode

This is the task Flutter should wait for.


builder

The builder is called every time the Future changes state.

builder: (context, snapshot)
Enter fullscreen mode Exit fullscreen mode

The snapshot contains information about the Future.


Step 4: Show a Loading Indicator

While waiting:

if (snapshot.connectionState ==
    ConnectionState.waiting) {
  return const CircularProgressIndicator();
}
Enter fullscreen mode Exit fullscreen mode

The user sees a loading spinner.


Step 5: Handle Errors

Sometimes requests fail.

if (snapshot.hasError) {
  return Text(
    snapshot.error.toString(),
  );
}
Enter fullscreen mode Exit fullscreen mode

Always handle errors so your app doesn't crash.


Step 6: Display the Data

When everything succeeds:

return Text(snapshot.data!);
Enter fullscreen mode Exit fullscreen mode

Output

Welcome John!
Enter fullscreen mode Exit fullscreen mode

Complete FutureBuilder Example

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  Future<String> fetchUser() async {
    await Future.delayed(
      const Duration(seconds: 3),
    );

    return "Welcome John!";
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text("FutureBuilder Demo"),
        ),
        body: Center(
          child: FutureBuilder<String>(
            future: fetchUser(),
            builder: (context, snapshot) {

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

              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }

              return Text(
                snapshot.data!,
                style: const TextStyle(fontSize: 22),
              );
            },
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the app and you'll see a loading spinner for 3 seconds before the welcome message appears.


Understanding the Snapshot

The snapshot tells you everything about the Future.

Is it still loading?

snapshot.connectionState ==
ConnectionState.waiting
Enter fullscreen mode Exit fullscreen mode

Did it finish?

snapshot.connectionState ==
ConnectionState.done
Enter fullscreen mode Exit fullscreen mode

Was there an error?

snapshot.hasError
Enter fullscreen mode Exit fullscreen mode

Is there data?

snapshot.hasData
Enter fullscreen mode Exit fullscreen mode

Get the data

snapshot.data
Enter fullscreen mode Exit fullscreen mode

Part 2: Understanding StreamBuilder

Unlike a Future, a Stream keeps sending new data.

Imagine a digital clock.

Every second, the time changes.

A StreamBuilder automatically rebuilds the UI whenever new data arrives.


Step 1: Create a Stream

Stream<int> counter() async* {

  for (int i = 1; i <= 10; i++) {

    await Future.delayed(
      const Duration(seconds: 1),
    );

    yield i;
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the keyword:

yield
Enter fullscreen mode Exit fullscreen mode

Instead of returning one value, it sends many values.


Step 2: Display the Stream

StreamBuilder<int>(
  stream: counter(),
  builder: (context, snapshot) {

    if (!snapshot.hasData) {
      return const CircularProgressIndicator();
    }

    return Text(
      snapshot.data.toString(),
    );
  },
)
Enter fullscreen mode Exit fullscreen mode

Step 3: Run the App

The screen updates automatically.

1

2

3

4

5

...

10
Enter fullscreen mode Exit fullscreen mode

No setState() is required.


Complete StreamBuilder Example

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  Stream<int> counter() async* {

    for (int i = 1; i <= 10; i++) {

      await Future.delayed(
        const Duration(seconds: 1),
      );

      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text("StreamBuilder Demo"),
        ),
        body: Center(
          child: StreamBuilder<int>(
            stream: counter(),
            builder: (context, snapshot) {

              if (!snapshot.hasData) {
                return const CircularProgressIndicator();
              }

              return Text(
                snapshot.data.toString(),
                style: const TextStyle(fontSize: 40),
              );
            },
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

FutureBuilder vs StreamBuilder

Feature FutureBuilder StreamBuilder
Receives one value
Receives multiple values
Stops automatically
Good for API calls
Good for live updates
Good for chat apps
Good for Firebase

Common Mistakes

Calling the Future Inside build()

Avoid creating a new Future every time the widget rebuilds.

❌ Don't do this:

FutureBuilder(
  future: fetchUser(),
)
Enter fullscreen mode Exit fullscreen mode

If the widget rebuilds frequently, fetchUser() may run multiple times.

A better approach is to create the Future once in initState() and reuse it.


Ignoring Errors

Always check:

snapshot.hasError
Enter fullscreen mode Exit fullscreen mode

This makes your app more reliable and helps with debugging.


Forgetting Loading States

Without a loading state, users may see a blank screen while waiting for data.

Always show something like:

const CircularProgressIndicator()
Enter fullscreen mode Exit fullscreen mode

Real-World Examples

Use FutureBuilder for:

  • Loading products from a REST API
  • Reading local files
  • Loading user profiles
  • Retrieving Hive or SQLite data once

Use StreamBuilder for:

  • Firebase Firestore documents
  • Live chat messages
  • GPS tracking
  • Download progress
  • Live notifications

Final Thoughts

FutureBuilder and StreamBuilder are fundamental Flutter widgets that make it easy to work with asynchronous data.

Remember this simple rule:

  • FutureBuilder → one-time data.
  • StreamBuilder → continuous data.

Mastering these widgets will help you build responsive apps that display loading indicators, handle errors gracefully, and update the UI automatically as data changes.

Top comments (0)