In this post we will learn to implement BLoC pattern with TextField of a Flutter app. We will use also use StreamController to receive user input and StreamHandler to show an error message if input is not valid.
Introduction
BLoC pattern is the preferred State Management pattern when developing mobile apps with Flutter. If you are completely new to BLoC pattern, you should checkout our other post on BLoC pattern with flutter.
Basically a BLoC pattern encourages separation of business logic from the user interface layer.
In this post we will create a TextField and handle it’s onChanged event using a stream controller implementing the BLoC pattern.
Setup
Let’s start off by creating a Flutter Material app with a single TextField.
import 'package:flutter/material.dart';
class MyTextFieldApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.white,
body: Container(
padding: EdgeInsets.all(24.0),
child: Center(
child: TextField(
decoration: InputDecoration(
fillColor: Colors.grey[100],
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(6.0),
),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Colors.blueAccent, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
focusedErrorBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Colors.red, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
),
),
))));
}
}
We have added some basic InputDecoration on this TextField as well.
See our previous post about learning to decorate a TexField here.
The output for the code above:

Setup A BLoC For The TextField
A BLoC is nothing but just another class. We will create a class named TextBloc which will look like this:
import 'dart:async';
class TextBloc {
var _textController = StreamController<String>();
Stream<String> get textStream => _textController.stream;
dispose() {
_textController.close();
}
}
This class has couple of things:
- StreamController
- This is responsible for adding data and error events on it’s stream.
- Stream
- Stream of string data.
Now to add data to the stream controller, we will also need a public function.
//TextBloc class
...
updateText(String text) {
(text == null || text == "")
? _textController.sink.addError("Invalid value entered!")
: _textController.sink.add(text);
}
...
Implementing BLoC With TextField
Now we can use the TextBloc in our app by first creating an instance of this bloc and using the updateText method during the onChanged event.
//init TextBloc
TextBloc _textBloc = TextBloc();
//implement the updateText method
...
Center(
child: TextField(
onChanged: (String text) => _textBloc.updateText(text),
decoration: InputDecoration(
...
As you can see, whenever a user enters value for the TextField, this value gets passed onto the StreamController inside our BLoC.
Learn To Use ValueNotifier Instead Of Streams
Showing Errors On TextField
Now that we have a working BLoC for our TextField, next we want to show errors whenever user enters invalid values.
In order to achieve this, we will have to listen to the text stream for the data entered by the user. For this purpose, we will make use of the StreamBuilder widget.
class MyTextFieldApp extends StatelessWidget {
final TextBloc _textBloc = TextBloc();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.white,
body: Container(
padding: EdgeInsets.all(24.0),
child: Center(
child: StreamBuilder(
stream: _textBloc.textStream,
builder: (ctxt, AsyncSnapshot<String> textStream) {
return TextField(
onChanged: (String text) =>
_textBloc.updateText(text),
decoration: InputDecoration(
errorText: textStream.hasError ? textStream.error : null,
fillColor: Colors.grey[100],
filled: true,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(6.0),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.blueAccent, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
errorBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Colors.red, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.redAccent, width: 0.0),
borderRadius: BorderRadius.circular(6.0),
),
),
);
}),
))));
}
}
Final Output:

Would be nice to have the code (github) to follow the articel.