Flutter state management with BLoC – simple approach

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

Leave a Reply

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