Introduction to StreamController
A StreamController
is a class in the dart:async
library that provides a way to create and manage streams of data in Dart and Flutter. A stream is a sequence of asynchronous events that can be listened to and processed over time.
With a StreamController
, you can create a stream of data, add events to the stream, and listen to the stream to receive those events. This makes it easy to build reactive, event-driven applications in Flutter.
There are two types of StreamController
in Dart and Flutter: a single-subscription controller and a broadcast controller. A single-subscription controller can only be listened to once, whereas a broadcast controller can be listened to multiple times.
Creating a StreamController
To create a StreamController
in Flutter, you can use the StreamController
constructor. The constructor takes two optional parameters: onListen
and onCancel
. The onListen
parameter is a callback that is called when the stream is first listened to, and the onCancel
parameter is a callback that is called when the last listener stops listening to the stream.
Here’s an example of how to create a StreamController
in Flutter:
StreamController<int> _controller = StreamController<int>.broadcast(); void dispose() { _controller.close(); }
In this example, we’re creating a broadcast StreamController
that emits int
events. We’re also defining a dispose
method that will be called when the widget is removed from the widget tree. In the dispose
method, we’re closing the StreamController
to prevent memory leaks.
Adding events to the StreamController
To add events to a StreamController
, you can use the add
method. The add
method takes a single argument that represents the event data to add to the stream.
Here’s an example of how to add events to a StreamController
:
_controller.add(1); _controller.add(2); _controller.add(3);
In this example, we’re adding three int
events to the StreamController
.
Listening to the StreamController
To listen to a StreamController
, you can use the stream
property to get the stream associated with the controller. You can then use the listen
method to receive events from the stream.
Here’s an example of how to listen to a StreamController
:
_controller.stream.listen((event) { print('Received event: $event'); });
In this example, we’re listening to the StreamController
‘s stream and printing out any received events.
Example: Counter app with StreamController
Here’s an example of how to use a StreamController
to build a simple counter app in Flutter.
import 'dart:async'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: CounterPage(), ); } } class CounterPage extends StatefulWidget { @override _CounterPageState createState() => _CounterPageState(); } class _CounterPageState extends State<CounterPage> { StreamController<int> _controller = StreamController<int>.broadcast(); int _counter = 0; @override void dispose() { _controller.close(); super.dispose(); } void _incrementCounter() { setState(() { _counter++; _controller.add(_counter); });
In the example, we’ve created a StreamController<int>
and a _counter
variable initialized to 0. When the user taps the “+” button, we increment the counter and add the new value to the StreamController
.
The build
method of CounterPage
returns a Scaffold
with a Column
as its body
. The Column
contains a Text
widget displaying the current counter value, and a RaisedButton
widget that calls the _incrementCounter
method when pressed.
We’re also listening to the StreamController
‘s stream in the initState
method of _CounterPageState
. Whenever a new value is added to the stream, we call setState
to update the UI.
@override void initState() { super.initState(); _controller.stream.listen((event) { setState(() { _counter = event; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Counter'), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$_counter', style: TextStyle(fontSize: 32), ), SizedBox(height: 16), RaisedButton( onPressed: _incrementCounter, child: Text('Increment'), ), ], ), ); }
Example: Search app with StreamController
Here’s another example of how to use a StreamController
to build a simple search app in Flutter.
import 'dart:async'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: SearchPage(), ); } } class SearchPage extends StatefulWidget { @override _SearchPageState createState() => _SearchPageState(); } class _SearchPageState extends State<SearchPage> { StreamController<String> _controller = StreamController<String>.broadcast(); String _query = ''; @override void dispose() { _controller.close(); super.dispose(); } void _onTextChanged(String text) { setState(() { _query = text; _controller.add(_query); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Search'), ), body: Column( children: [ TextField( onChanged: _onTextChanged, decoration: InputDecoration( labelText: 'Search', ), ), Expanded( child: StreamBuilder<String>( stream: _controller.stream, builder: (context, snapshot) { if (!snapshot.hasData) { return Center(child: Text('Enter a search query')); } final query = snapshot.data; final results = _search(query); return ListView.builder( itemCount: results.length, itemBuilder: (context, index) { final result = results[index]; return ListTile( title: Text(result), ); }, ); }, ), ), ], ), ); } List<String> _search(String query) { if (query.isEmpty) { return []; } return [ 'Result 1 for "$query"', 'Result 2 for "$query"', 'Result 3 for "$query"', ]; } }
In this example, we’re creating a StreamController<String>
and a _query
variable initialized to an empty string. Whenever the user types in the search field, we update the query and it to the StreamController
. We’re also listening to the StreamController
‘s stream in the StreamBuilder
widget. Whenever a new value is added to the stream, we call the _search
method to get the search results, and display them in a ListView
.
The build
method of SearchPage
returns a Scaffold
with a Column
as its body
. The Column
contains a TextField
widget for the user to input the search query, and a StreamBuilder
widget that displays the search results.
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Search'), ), body: Column( children: [ TextField( onChanged: _onTextChanged, decoration: InputDecoration( labelText: 'Search', ), ), Expanded( child: StreamBuilder<String>( stream: _controller.stream, builder: (context, snapshot) { if (!snapshot.hasData) { return Center(child: Text('Enter a search query')); } final query = snapshot.data; final results = _search(query); return ListView.builder( itemCount: results.length, itemBuilder: (context, index) { final result = results[index]; return ListTile( title: Text(result), ); }, ); }, ), ), ], ), ); }
We’re using the broadcast
constructor for the StreamController
because we want multiple listeners to be able to listen to the stream. We’re also disposing the StreamController
in the dispose
method of _SearchPageState
.
class _SearchPageState extends State<SearchPage> { StreamController<String> _controller = StreamController<String>.broadcast(); // ... @override void dispose() { _controller.close(); super.dispose(); } // ... }
Example: Login form with StreamController
Here’s another example of how to use a StreamController
to validate a login form in Flutter.
import 'dart:async'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: LoginPage(), ); } } class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { StreamController<String> _emailController = StreamController<String>(); StreamController<String> _passwordController = StreamController<String>(); StreamController<bool> _formValidController = StreamController<bool>(); String _email = ''; String _password = ''; @override void dispose() { _emailController.close(); _passwordController.close(); _formValidController.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Login'), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( onChanged: (value) { setState(() { _email = value; _emailController.add(value); _validateForm(); }); }, decoration: InputDecoration( labelText: 'Email', ), ), TextField( onChanged: (value) { setState(() { _password = value; _passwordController.add(value); _validateForm(); }); }, decoration: InputDecoration( labelText: 'Password', ), obscureText: true, ), SizedBox(height: 16), StreamBuilder<bool>( stream: _formValidController.stream, initialData: false, builder: (context, snapshot) { return ElevatedButton( onPressed: snapshot.data ? () {} : null, child: Text('Login'), ); }, ), ], ), ); } void _validateForm() { final email = _email.trim(); final password = _password.trim(); final emailValid = _emailValidator(email); final passwordValid = _passwordValidator(password); _formValidController.add(emailValid && passwordValid); } bool _emailValidator(String email) { return email.isNotEmpty && email.contains('@'); } bool _passwordValidator(String password) { return password.isNotEmpty && password.length >= 6; } }
In this example, we’re using three StreamController
objects to validate a login form. We’re listening to the onChanged
event of the email and password TextField
widgets, and adding the entered values to the respective StreamController
objects.
We’re also calling a _validateForm
method whenever the email or password changes. This method checks if the email and password are valid, and adds the result to the _formValidController
stream.
Finally, we’re listening to the _formValidController
stream in a StreamBuilder
widget, and enabling or disabling the Login button based on the latest value of the stream.
Conclusion
In this post, we’ve seen how to use a StreamController
in Flutter to create and manage a stream of data. We’ve also seen some examples of how to use StreamController
to implement various features in a Flutter app, such as search and form validation.
I hope this post has been helpful to you in understanding how to work with StreamController
in Flutter. As always, if you have any questions or comments, please feel free to leave them below.