Flutter Tutorial

Flutter Intermediate Lesson 64 of 135 3 min read

StreamBuilder in Flutter

Learn StreamBuilder in Flutter: rebuild UI for every stream event, handle connection states, and use it for timers, sockets and live data.

On this page

A FutureBuilder handles one value that arrives later. A StreamBuilder handles many: it rebuilds every time the stream emits. Use it for anything live, such as a timer, a chat, a download’s progress or a database that pushes changes.

If streams are new to you, read the Dart lesson on streams first.

A live clock

import 'package:flutter/material.dart';

Stream<DateTime> clock() async* {
  while (true) {
    yield DateTime.now();
    await Future<void>.delayed(const Duration(seconds: 1));
  }
}

void main() => runApp(const MaterialApp(home: ClockPage()));

class ClockPage extends StatefulWidget {
  const ClockPage({super.key});

  @override
  State<ClockPage> createState() => _ClockPageState();
}

class _ClockPageState extends State<ClockPage> {
  late final Stream<DateTime> _clock = clock(); // created once

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StreamBuilder<DateTime>(
          stream: _clock,
          builder: (context, snapshot) {
            if (!snapshot.hasData) return const CircularProgressIndicator();
            final t = snapshot.data!;
            String two(int n) => n.toString().padLeft(2, '0');
            return Text(
              '${two(t.hour)}:${two(t.minute)}:${two(t.second)}',
              style: const TextStyle(fontSize: 56),
            );
          },
        ),
      ),
    );
  }
}

StreamBuilder subscribes when it appears and cancels the subscription when it is removed. You do not manage the subscription yourself.

Connection states

connectionStateMeaning
noneNo stream was given
waitingSubscribed, nothing received yet
activeAt least one event has arrived, and more may come
doneThe stream has closed
builder: (context, snapshot) {
  if (snapshot.hasError) return Text('Error: ${snapshot.error}');
  if (snapshot.connectionState == ConnectionState.waiting) {
    return const CircularProgressIndicator();
  }
  if (!snapshot.hasData) return const Text('No data');
  return Text('${snapshot.data}');
}

initialData

If you know a sensible starting value, pass it and skip the waiting state.

StreamBuilder<int>(
  stream: _unreadCount,
  initialData: 0,
  builder: (context, snapshot) => Badge(
    label: Text('${snapshot.data}'),
    child: const Icon(Icons.mail),
  ),
)

The same mistake as FutureBuilder

Do not create the stream inside build. Each rebuild would create a new stream and resubscribe, and for a network connection that means reconnecting.

// Wrong
StreamBuilder(stream: clock(), builder: ...)

// Right: create once in the State, then pass the field
StreamBuilder(stream: _clock, builder: ...)

Your own stream of events

A StreamController lets you push values in from your own code.

import 'dart:async';

class DownloadService {
  final _progress = StreamController<double>.broadcast();

  Stream<double> get progress => _progress.stream;

  Future<void> download() async {
    for (var i = 0; i <= 100; i += 5) {
      await Future<void>.delayed(const Duration(milliseconds: 150));
      _progress.add(i / 100);
    }
  }

  void dispose() => _progress.close();
}
StreamBuilder<double>(
  stream: _service.progress,
  initialData: 0,
  builder: (context, snapshot) {
    final value = snapshot.data!;
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        LinearProgressIndicator(value: value),
        const SizedBox(height: 8),
        Text('${(value * 100).round()}%'),
      ],
    );
  },
)

Where streams come from in real apps

SourceStream of
Firebase Firestore .snapshots()The live contents of a collection or document
Firebase Auth authStateChanges()The current user, or null
A WebSocket channelMessages from a server
Local databases such as Drift and IsarQuery results that update when data changes
connectivity_plusOnline and offline changes
Sensors and locationReadings
// Decide which screen to show based on login state.
StreamBuilder<User?>(
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const SplashScreen();
    }
    return snapshot.hasData ? const HomePage() : const LoginPage();
  },
)

Errors do not end the builder

When a stream emits an error, the snapshot carries it and the builder runs. Later events still arrive. Always handle hasError.

StreamBuilder or a state holder?

StreamBuilder is fine for showing a stream directly. When the data needs processing, combining with other state, or sharing across screens, subscribe in a state holder and expose plain state. Riverpod’s StreamProvider and Bloc’s emit.forEach do this for you.

Try it yourself

Build a countdown timer: a stream that emits the remaining seconds from 10 to 0, shown in large text, with a “Done!” message when the stream closes, and a button that restarts it by creating a new stream in setState.

Practise in the playground Updated