Back to Posts

Flutter StreamBuilder: Handling Async Data Like a Pro

15 min read
<div style="text-align: center;"> <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDMwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPCEtLSBTdHJlYW1CdWlsZGVyIGV4YW1wbGUgLS0+CiAgPHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNGRkYiIHN0cm9rZT0iIzAwMCIvPgogIDx0ZXh0IHg9IjE1MCIgeT0iMTAwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMyMTIxMjEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlN0cmVhbUJ1aWxkZXI8L3RleHQ+Cjwvc3ZnPg==" alt="StreamBuilder Example" width="300" /> </div>

StreamBuilder is a powerful widget in Flutter that helps you handle asynchronous data streams and update your UI in real-time. This guide will show you how to effectively use StreamBuilder with practical examples and best practices.

Basic StreamBuilder Usage

1. Simple Counter Example

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

  @override
  State<CounterStream> createState() => _CounterStreamState();
}

class _CounterStreamState extends State<CounterStream> {
  final StreamController<int> _controller = StreamController<int>();
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _controller.add(_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter Stream')),
      body: Center(
        child: StreamBuilder<int>(
          stream: _controller.stream,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return Text('Error: ${snapshot.error}');
            }
            if (!snapshot.hasData) {
              return const CircularProgressIndicator();
            }
            return Text(
              'Count: ${snapshot.data}',
              style: Theme.of(context).textTheme.headlineMedium,
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _counter++;
          _controller.add(_counter);
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }
}

2. Data Loading Example

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Data Loader')),
      body: StreamBuilder<List<String>>(
        stream: Stream.fromFuture(_fetchData()),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          }
          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
          return ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(snapshot.data![index]),
              );
            },
          );
        },
      ),
    );
  }
}

Advanced StreamBuilder Patterns

1. Multiple Streams

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

  Stream<int> _counterStream() async* {
    int count = 0;
    while (true) {
      await Future.delayed(const Duration(seconds: 1));
      yield count++;
    }
  }

  Stream<String> _textStream() async* {
    final texts = ['Hello', 'World', 'Flutter'];
    int index = 0;
    while (true) {
      await Future.delayed(const Duration(seconds: 2));
      yield texts[index];
      index = (index + 1) % texts.length;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Multiple Streams')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          StreamBuilder<int>(
            stream: _counterStream(),
            builder: (context, snapshot) {
              return Text(
                'Counter: ${snapshot.data ?? 0}',
                style: Theme.of(context).textTheme.headlineMedium,
              );
            },
          ),
          const SizedBox(height: 20),
          StreamBuilder<String>(
            stream: _textStream(),
            builder: (context, snapshot) {
              return Text(
                'Text: ${snapshot.data ?? ""}',
                style: Theme.of(context).textTheme.headlineMedium,
              );
            },
          ),
        ],
      ),
    );
  }
}

2. Stream Transformation

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

  Stream<int> _numberStream() async* {
    for (int i = 1; i <= 10; i++) {
      await Future.delayed(const Duration(seconds: 1));
      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Stream Transformation')),
      body: StreamBuilder<int>(
        stream: _numberStream()
            .map((number) => number * 2)
            .where((number) => number % 3 == 0),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const Center(child: CircularProgressIndicator());
          }
          return Center(
            child: Text(
              'Transformed: ${snapshot.data}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          );
        },
      ),
    );
  }
}

Error Handling and State Management

1. Error Handling

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

  Stream<int> _errorProneStream() async* {
    for (int i = 1; i <= 5; i++) {
      await Future.delayed(const Duration(seconds: 1));
      if (i == 3) {
        throw Exception('Simulated error');
      }
      yield i;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Error Handling')),
      body: StreamBuilder<int>(
        stream: _errorProneStream(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.error, color: Colors.red, size: 48),
                  const SizedBox(height: 16),
                  Text(
                    'Error: ${snapshot.error}',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                ],
              ),
            );
          }
          if (!snapshot.hasData) {
            return const Center(child: CircularProgressIndicator());
          }
          return Center(
            child: Text(
              'Value: ${snapshot.data}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          );
        },
      ),
    );
  }
}

2. State Management with StreamBuilder

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: Scaffold(
        appBar: AppBar(title: const Text('State Management')),
        body: Consumer<CounterModel>(
          builder: (context, model, child) {
            return StreamBuilder<int>(
              stream: model.counterStream,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return const Center(child: CircularProgressIndicator());
                }
                return Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        'Count: ${snapshot.data}',
                        style: Theme.of(context).textTheme.headlineMedium,
                      ),
                      const SizedBox(height: 20),
                      ElevatedButton(
                        onPressed: model.increment,
                        child: const Text('Increment'),
                      ),
                    ],
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }
}

class CounterModel extends ChangeNotifier {
  final StreamController<int> _controller = StreamController<int>();
  int _count = 0;

  Stream<int> get counterStream => _controller.stream;
  int get count => _count;

  CounterModel() {
    _controller.add(_count);
  }

  void increment() {
    _count++;
    _controller.add(_count);
    notifyListeners();
  }

  @override
  void dispose() {
    _controller.close();
    super.dispose();
  }
}

Best Practices

  1. Stream Management

    • Always close streams when done
    • Use appropriate stream controllers
    • Handle stream errors properly
  2. Performance

    • Avoid unnecessary stream subscriptions
    • Use appropriate stream transformations
    • Implement proper error handling
  3. State Management

    • Choose appropriate state management solution
    • Keep streams focused and specific
    • Handle stream lifecycle properly
  4. User Experience

    • Show loading states
    • Handle errors gracefully
    • Provide feedback for actions

Common Issues and Solutions

  1. Stream Not Closing

    @override
    void dispose() {
      _controller.close();
      super.dispose();
    }
  2. Memory Leaks

    final _subscription = stream.listen((data) {
      // Handle data
    });
    
    @override
    void dispose() {
      _subscription.cancel();
      super.dispose();
    }
  3. Error Handling

    StreamBuilder(
      stream: stream.handleError((error) {
        // Handle error
      }),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return ErrorWidget(snapshot.error!);
        }
        // Build widget
      },
    )

Conclusion

StreamBuilder is a powerful tool for handling asynchronous data in Flutter. Remember to:

  • Properly manage stream lifecycle
  • Handle errors gracefully
  • Implement appropriate state management
  • Follow best practices for performance

Happy coding!