This article presents how to implement BLoC in Flutter.
The context on example:
- Give an edit text to get data input
- Handle changing the text to get final search text
- Deal with focus issues
- Call Rest API to get data and stream it to the UI without setState
About State Management in Flutter please read my old article on medium here:
https://medium.com/beesightsoft/flutter-state-management-2455c60cc423
Let’s begin

The UI has 3 main parts: Input text, Loading progress, and the result list or ‘no data’ text
Input text
child: TextField(
onChanged: bloc.searchUser.push,
autofocus: true,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Please enter a search term',
),
),
Loading view
Container(
child: StreamBuilder(
stream: bloc.loading.stream,
builder: (context, loading) {
if (loading.hasData && loading.data) {
return Center(
child: CircularProgressIndicator(),
);
}
return Container();
},
),
),
The result part
Expanded(
child: StreamBuilder(
stream: bloc.searchUser.stream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
if (!snapshot.hasData || (snapshot?.data)?.length == 0) {
return Text('No data');
}
List<UserBase> users = snapshot.data;
return ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
return FlatButton(
child: Row(
children: <Widget>[
CircleAvatar(
backgroundImage:
NetworkImage(users[index].avatarUrl),
radius: 20.0,
),
Padding(
padding: EdgeInsets.only(left: 10, right: 10),
child: Text('${users[index].login}'),
),
],
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailScreen(userBase: users[index])));
},
);
},
);
},
),
),
Next is the BLoC Design Pattern (Business Logic Component)
import 'package:rxdart/rxdart.dart';
class Bloc<I, O> {
// @nhancv 2019-10-04: Input data driven
final BehaviorSubject<I> _inputSubject = BehaviorSubject<I>();
// @nhancv 2019-10-04: Output data driven
final BehaviorSubject<O> _outputSubject = BehaviorSubject<O>();
// @nhancv 2019-10-04: Dynamic logic
// Transfer data from input to mapper to output
set logic(Observable<O> Function(Observable<I> input) mapper) {
mapper(_inputSubject).listen(_outputSubject.sink.add);
}
// @nhancv 2019-10-04: Push input data to BLoC
void push(I input) => _inputSubject.sink.add(input);
// @nhancv 2019-10-04: Stream output from BLoC
Stream<O> get stream => _outputSubject;
// @nhancv 2019-10-04: Dispose BLoC
void dispose() {
_inputSubject.close();
_outputSubject.close();
}
}
BLoC will take input from UI and mapping input to output data type and stream it to UI via outputSubject.
In the navigated widget the widget will be rebuilt reflect with input focus state. In BLoC implemented above, when user input to a text field, the input subject will send it to the mapper function to the output format and push it to output subject. BehaviorSubject will cache the latest data when widget rebuilds the stream just take the latest result and render without run mapper again.
The SearchBloc
import 'dart:convert';
import 'package:bflutter/bflutter.dart';
import 'package:bflutter_poc/api.dart';
import 'package:bflutter_poc/model/user_base.dart';
import 'package:flutter/cupertino.dart';
import 'package:rxdart/rxdart.dart';
class SearchBloc {
final loading = BlocDefault<bool>();
final searchUser = Bloc<String, List<UserBase>>();
SearchBloc() {
_initSearchUserLogic();
}
void _initSearchUserLogic() {
searchUser.logic = (Observable<String> input) => input
.distinct()
.debounceTime(Duration(milliseconds: 500))
.flatMap((input) {
//show loading
loading.push(true);
if (input.isEmpty) return Observable.just(null);
return Observable.fromFuture(Api().searchUsers(input));
}).map((data) {
if (data == null) {
return <UserBase>[];
}
if (data.statusCode == 200) {
final List<UserBase> result = json
.decode(data.body)['items']
.cast<Map<String, dynamic>>()
.map<UserBase>((item) => UserBase.fromJson(item))
.toList();
return result;
} else {
throw (data.body);
}
}).handleError((error) {
loading.push(false);
throw error;
}).doOnData((data) {
loading.push(false);
});
}
void dispose() {
loading.dispose();
searchUser.dispose();
}
}
How to use SearchBloc on screen
class ___SearchInfoState extends State<_SearchInfo> {
final bloc = SearchBloc();
@override
void dispose() {
bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {}
}
Completed source here
https://github.com/beesightsoft/bflutter