How to work with StreamController in Flutter.

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.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *