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
-
Stream Management
- Always close streams when done
- Use appropriate stream controllers
- Handle stream errors properly
-
Performance
- Avoid unnecessary stream subscriptions
- Use appropriate stream transformations
- Implement proper error handling
-
State Management
- Choose appropriate state management solution
- Keep streams focused and specific
- Handle stream lifecycle properly
-
User Experience
- Show loading states
- Handle errors gracefully
- Provide feedback for actions
Common Issues and Solutions
-
Stream Not Closing
@override void dispose() { _controller.close(); super.dispose(); }
-
Memory Leaks
final _subscription = stream.listen((data) { // Handle data }); @override void dispose() { _subscription.cancel(); super.dispose(); }
-
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!