Dart Tutorial

Flutter Lesson 45 of 83 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 by Santosh Adhikari