Flutter: Working With BLoC using Reactive Programming

In this tutorial, we will learn Managing App State in Flutter using BLoC pattern and Reactive Programming Rx. We will use Shopping Cart as an example in this post.

What is BLoC Pattern?

BLoC stands for Business Logic Pattern in which you put all your business logic side implementation in one place that receives events/data from Network parts, delivers to UI screens and back and forth.

This means UI part should not be handling any logic even including data validations. The reason for doing this is simple: Write Once Use Everywhere.

DecisionMentor app

But BLoC is not only limited to this. It specifies using Reactive Programming as underlying technology to handle events/data flow.

So, What is Reactive Programming?

Well there are more than couple of terms that needs to be defined before Reactive Programming but simply to put: Its a way of solving problems in which you handle data streams asynchronously.

Describing about Reactive Programming will need a separate post and we don’t want to overwhelm you for now. We will write a separate post for this.

Below is the pictorial description of how BLoC pattern will work.

BLoC Pattern using Reactive Programming
BLoC Pattern using Reactive Programming

Shopping Cart Using BLoC Pattern

Now that we have some understanding of BLoC pattern, we will implement Shopping Cart using this pattern.

But before we start, lets describe what problem we are going to solve. Any retail store app will have a shopping cart and we will work on only two requirements:

  1. When user double taps a product, it should go inside cart.
  2. When user double taps same product, it should be removed from cart.

After defining above requirements, we list our Data Model and Actions.

  • Data Model: Product
  • Actions: Add Product, Remove Product

Lets define our Model first:

// lib/models/product.dart
class Product {
  final int id;
  final String name;

  Product({this.id, this.name});
} 

Add rxdart package to support reactive programming

// pubsec.yaml
...
dependencies:
  ...
  rxdart: 0.18.0

Now define Shopping Cart BLoC

// lib/blocs/shopping_cart_bloc.dart
import 'dart:core';
import 'package:rxdart/rxdart.dart';

import '../models/product.dart';

class ShoppingCartBloc {

  final _productListController = BehaviorSubject<List<Product>>(seedValue: []);
  Stream<List<Product>> get productListStream => _productListController.stream;

  // action
  updateProductList(List<Product> productList) => _productListController.sink.add(productList);

  dispose() {
    _productListController.close();
  }
}

Here, we try to process products selected by user. We pass product list to _productListController using updateProductList function and then receive data only from its stream. Remember that it doesn’t store any data. Any components that need selected product should subscribe to productListStream. It will emit productList to any listener it has subscribed.

Also, remember to close controller in dispose method.

Since we have implemented our business logic inside BLoC, we need to have access to it in any widget – at any level of widget tree. But How?

InheritedWidget to the rescue. We add our BLoCs into Inherited Widget and access their stream where we need it.

Learn How Inherited Widget works

// lib/app_state_provider.dart
import 'package:flutter/material.dart';

import './blocs/shopping_cart_bloc.dart';

class AppState extends InheritedWidget {

  final AppStateData data;

  AppState({
    Key key,
    @required this.data,
    @required Widget child,
  }) :
    assert(data != null),
    assert(child != null),
    super(key: key, child: child);

  @override
  bool updateShouldNotify(_) => false; // no need to rebuild listeners

  static AppStateData of(BuildContext context) =>
    (context.inheritFromWidgetOfExactType(AppState) as AppState).data;
}

class AppStateData {

  final ShoppingCartBloc shoppingCartBloc;

  AppStateData({
    @required this.shoppingCartBloc,
  }) : assert(shoppingCartBloc != null);
}

And below is the widget that actually uses Shopping Cart BLoC

// lib/screens/shopping_cart.dart
import 'package:flutter/material.dart';

import '../models/product.dart';
import '../app_state_provider.dart';

final _kProductList = <Product>[
  Product(id: 1, name: 'Product A'),
  Product(id: 2, name: 'Product B'),
  Product(id: 3, name: 'Product C'),
];

class ShoppingCart extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    // get application state to access shopping cart bloc
    final AppStateData appState = AppState.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping Cart Demo')
      ),
      body: StreamBuilder(
        stream: appState.shoppingCartBloc.productListStream,
        builder : (BuildContext _, AsyncSnapshot<List<Product>> sptBody) {

          // show progress indicator if data is not yet received
          if (!sptBody.hasData) {
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          // show error if stream have some error
          if (sptBody.hasError) {
            return Center(
              child: Text('Oops. Something went wrong!'),
            );
          }

          return ListView(
            children: _kProductList.map((product) =>
              GestureDetector(
                onDoubleTap: () {

                  // if product already exists in cart
                  // remove from list
                  // else add it to list
                  bool productExistsInCart = sptBody.data.any((p) => p.id == product.id);
                  List<Product> newProductList;
                  if (productExistsInCart) {
                    newProductList = sptBody.data.where((p) => p.id != product.id).toList();
                  } else {
                    newProductList = sptBody.data + [product];
                  }

                  // send updated product list to stream
                  appState.shoppingCartBloc.updateProductList(newProductList);
                },
                child: ListTile(
                  title: Text(product.name),
                ),
              )
            ).toList(),
          );
        },
      ),
      floatingActionButton: StreamBuilder(
        stream: appState.shoppingCartBloc.productListStream,
        builder: (BuildContext cxtProductList, AsyncSnapshot<List<Product>> sptProductList) {

          // any changes to productListStream in shopping cart bloc
          // is also reflected here
          return FloatingActionButton(
            onPressed: sptProductList.hasData ? () {} : null,
            tooltip: 'Shopping Cart',
            child: Text('${sptProductList.data?.length}'),
          );
        },
      ),
    );
  }
}

In above example, we showed the list of products and a floating button which will display number of products selected. When any product is double tapped, if it already exists in product list of shopping cart it will be removed else it will be added to it. Remember Add Product and Remove Product actions in our requirement.

StreamBuilder

You might be wondering what are those builder widgets and why are we wrapping both ListView and FloatingActionButton.

Definition

StreamBuilder: Widget that builds itself based on the latest snapshot of interaction with a Stream

Flutter Docs

StreamBuilder subscribes to the stream attached to it. That means, it will listen to events/data emitted by the attached stream and calls builder function every time new data arrive.

How it works?

Well remember that, productListStream is a Stream type. That means it will continuously emit any data added to its source. Our source is _productListController where we add data from updateProductList function.

In our case, every time we call updateProductList from Shopping Cart screen, we add new data to our source _productListController. And then subscribed to productListStream using StreamBuilder to update UI screen accordingly.

Running the app

// lib/main.dart
import 'package:flutter/material.dart';

import 'app_state_provider.dart';
import 'blocs/shopping_cart_bloc.dart';
import './screens/shopping_cart.dart';

void main() => runApp(ShoppingApp());

class ShoppingApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return AppState(
      data: AppStateData(
        shoppingCartBloc: ShoppingCartBloc(),
      ),
      child: MaterialApp(
        title: 'BLoC Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: ShoppingCart(),
      ),
    );
  }
}

As you see, we show ShoppingCart in our home screen. We provide ShoppingCartBloc to AppState and wrap our main app around it so that our app state can be accessed from any widget.

If you are new to InheritedWidget, we suggest you reading our tutorial in Basics of Inherited Widget.

Shopping Cart Demo using BLoC Pattern

Wrapping Up

We showcased the simple way of how to combine BLoC, Reactive Stream in Application State. Designing this way will help you scale our app easily. You separate all your business logic inside BLoC, put them in Application State and use it from anywhere. Any change in data will be reflected by builder function of StreamBuilder.