When developing a Flutter application, you may encounter the need to split a large UI component into several smaller components to improve the readability of the code. For multiple components, effective communication between them is critical. All UI components should be aware of the state of the application at all times. This is called state management.

In Flutter, you can manage your application’s state by using setState. But while setState can be your best friend, relying solely on it is not a good idea. There are many other factors you should consider when developing Flutter applications, such as architecture, extensibility, readability, complexity, etc. Mastering everything requires an effective state management technique.

There are countless state management solutions available for Flutter, including Provider, InheritedWidget and InheritedModel, Redux, BLoC, GetIt, MobX, Riverpod, and more. In this tutorial, we will focus on state management using BLoC design mode in Flutter. We’ll explain what BLoC means and show you how to do anything with BLoC.

What’s BLoC?

The Business Logic Component (BLoC) allows you to separate business logic from the user interface. Coding with BLoC makes it easier to write and reuse tests.

Simply put, BLoC takes a stream of events, processes the data according to the events, and produces outputs as states. Take this simple example.

Once the rotation 90° button is clicked, RotateEvent, which is sent to BLoC, represents the rotation state, the RotatedState, is emitted. The triangular part rotates itself when it receives the RotatedState from BLoC. Similarly, the circular widget changes color when the “Change color to red” button is clicked.

Since BLoC handles rotation and color change operations, these two operations can be performed on any widget. This is good for code reuse.

Important BLoC concept

Before we delve further, let’s review some basic concepts and terminology of BLoC so that we have a common understanding.

The event

The event tells BLoC what to do. An event can be triggered from anywhere, such as from a UI widget. External events, such as changes in network connections, changes in sensor readings, and so on, look like this.

class RotateEvent {
  final double angle;

  const RotateEvent(this.angle);

  @override
  List<Object> get props => [angle];
}

Copy the code

BLoC

BLoC is a middle person. All the business logic is located in the BLoC file. It simply accepts events, executes logic, and outputs state. Here’s what it looks like.

class TransformationBloc extends Bloc<TransformationEvent, TransformationState> { TransformationBloc() : super(RotatedState(angle: 0); @override Stream<TransformationState> mapEventToState( TransformationEvent event) async* { if (event is RotateEvent) { yield RotatedState(angle: event.angle); }}}Copy the code

state

States represent information to be processed by any part. A widget changes itself based on state.

class RotatedState {
  final double angle;

  const RotatedState({@required this.angle});

  @override
  List<Object> get props => [angle];
}

Copy the code

Cubit

Cubit is a simpler version of the BLoC model. It eliminates the need to write events.

Cubit exposes direct functions, which can lead to appropriate states. Writing Cubit instead of BLoC also reduces the template code, making it easier to read.

Here’s a simple example.

class TransformCubit extends Cubit<TransformState> { TransformCubit() : super(RotatedState(angle: 0)); void rotate(double angle) { emit(RotatedState(angle: angle)); }}Copy the code

In the Flutter withsetState(No BLoC) Management status

Before we emphasize the benefits of using BLoC to manage state in Flutter, let’s look at the process of replacing state management with setState.

Our example Flutter application will display a list of available products. Products can be added or removed from the cart by clicking on the icon next to the product name. The number of items in the cart will be updated accordingly.

With setState, the entire UI is decomposed into three classes.

  1. home.dartIs to accommodate the bracket andAppBarThe main file of. AppBar contains the shopping cart icon widget
  2. product_list.dartDisplays a list of products
  3. product_tile.dartDisplays individual product items.

Here’s what it looks like.

The list of items in the cart is passed from Home (top) to the ProductTile (bottom) widget to check if an item has exited the cart. If it is, the shopping cart icon will be highlighted.

Click the shopping cart icon next to the product name to add the item to your shopping cart. The callback to refresh the shopping cart icon on AppBar is from ProductTile (bottom) to Home (top).

The problem ofsetState

The setState management approach in Flutter works well for simple applications with only a few components. However, for more complex actual Flutter applications with a deep tree of parts, using setState causes the following problems.

  • Code duplication – Data must be passed from all parts to the bottom, making code difficult to read
  • Performance degrades because willsetStatePromotion to a parent with deep structure leads to unnecessary redrawing

How to use BLoC to manage states in a Flutter

Now let’s do the same thing with BLoC.

First, add BLoC library.

"> < span style =" font-size: 14px; line-height: 20pxCopy the code

Next, create and add a BLoC observer. This helps you determine the order of events and states that have occurred, which is great for debugging applications.

void main() { Bloc.observer = SimpleBlocObserver(); runApp(MyApp()); } import 'package:flutter_bloc/flutter_bloc.dart'; /// Custom [BlocObserver] which observes all bloc and cubit instances. class SimpleBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object event) { super.onEvent(bloc, event); print(event); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print(transition); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print(error); super.onError(bloc, error, stackTrace); }}Copy the code

Create events to add and remove products from the shopping cart item list.

import 'package:equatable/equatable.dart';

abstract class CartEvent extends Equatable {
  const CartEvent();

  @override
  List<Object> get props => [];
}

class AddProduct extends CartEvent {
  final int productIndex;
  const AddProduct(this.productIndex);
  @override
  List<Object> get props => [productIndex];
  @override
  String toString() => 'AddProduct { index: $productIndex }';
}

Copy the code

Now, create a state that indicates that the product has been added and removed.

import 'package:flutter/material.dart';

abstract class CartState {
  final List<int> cartItem;
  const CartState({@required this.cartItem});

  @override
  List<Object> get props => [];
}

class ProductAdded extends CartState {
  final List<int> cartItem;
  const ProductAdded({@required this.cartItem}) : super(cartItem: cartItem);

  @override
  List<Object> get props => [cartItem];
  @override
  String toString() => 'ProductAdded { todos: $cartItem }';
}

Copy the code

Write business logic to add and remove products to cartItems and issue corresponding states. The actual list of items in the shopping cart is maintained at BLoC level.

class CartBloc extends Bloc<CartEvent, CartState> { CartBloc() : super(ProductAdded(cartItem: [])); final List<int> _cartItems = []; List<int> get items => _cartItems; @override Stream<CartState> mapEventToState(CartEvent event) async* { if (event is AddProduct) { _cartItems.add(event.productIndex); yield ProductAdded(cartItem: _cartItems); } else if (event is RemoveProduct) { _cartItems.remove(event.productIndex); yield ProductRemoved(cartItem: _cartItems); }}}Copy the code

Next, put the scaffold components wrapped in [BlocProvider] (https://pub.dev/packages/flutter_bloc#blocprovider).

BlocProvider is a Flutter widget that makes any BLoC available to the entire widget tree below it. In our example, any widget between Home (top) and ProductTile (bottom) can access the shopping cart, so there is no need to pass the shopping cart data from the top of the widget tree to the bottom.

BlocProvider(
    create: (_) => CartBloc(),
    child: Scaffold(
      appBar: CartCounter(),
      body: ProductList(),
    ));

Copy the code

Wrap the shopping cart icon and product list in BlocBuilder. BlocBuilder only needs to rebuild its internal widgets when it receives BLoC’s new state.

// Cart icon
BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
  List<int> cartItem = cartState.cartItem;
  return Positioned(
    left: 30,
    child: Container(
      padding: EdgeInsets.all(5),
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(10),
          color: Colors.red),
      child: Text(
        '${cartItem.length}',
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
    ),
  );
}),
//Product list
 BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
  List<int> cart = cartState.cartItem;
  return LayoutBuilder(builder: (context, constraints) {
    return GridView.builder(
      itemCount: 100,
      itemBuilder: (context, index) => ProductTile(
        itemNo: index,
        cart: cart,
      ),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: constraints.maxWidth > 700 ? 4 : 1,
        childAspectRatio: 5,
      ),
    );
  });
});

Copy the code

Note: The BlocBuilder for CartBloc is only added in two places, because we only want these two widgets to rebuild if the CartBloc changes. This approach of refreshing only the widgets needed greatly reduces the amount of unnecessary redrawing.

The next step is to photograph events to the CartBloc for adding and removing items from the cart. BlocProvider. Of

(context) finds the most recent instance of CartBloc in the widget tree and adds events to it.

IconButton(
  key: Key('icon_$itemNo'),
  icon: cart.contains(itemNo)
      ? Icon(Icons.shopping_cart)
      : Icon(Icons.shopping_cart_outlined),
  onPressed: () {
    !cart.contains(itemNo)
        ? BlocProvider.of<CartBloc>(context).add(AddProduct(itemNo))
        : BlocProvider.of<CartBloc>(context).add(RemoveProduct(itemNo));
  },
)

Copy the code

Now replace BlocBuilder with BlocConsumer. BlocConsumer allows us to rebuild the widget and react to the state. It should only be used when you want to rebuild the widget and perform some actions.

In our example, we want to refresh the list and display a cart when products are added or removed from the cart.

BlocConsumer<CartBloc, CartState>(
listener: (context, state) { 
  Scaffold.of(context).showSnackBar(
    SnackBar(
      content: Text(
          state is ProductAdded ? 'Added to cart.' : 'Removed from cart.'),
      duration: Duration(seconds: 1),
    ),
  );
}, 
builder: (_, cartState) {
  List<int> cart = cartState.cartItem;
  return LayoutBuilder(builder: (context, constraints) {
    return GridView.builder();
  });
});

Copy the code

Alternatively, if you want to reduce some of the template code and the order of the states doesn’t matter to you, try Cubit. Here’s what CartCubit looks like.

class CartCubit extends Cubit<CartState> { CartCubit() : super(ProductAdded(cartItem: [])); final List<int> _cartItems = []; List<int> get items => _cartItems; void add(int productIndex) { _cartItems.add(productIndex); emit (ProductAdded(cartItem: _cartItems)); } void remove(int productIndex) { _cartItems.remove(productIndex); emit (ProductRemoved(cartItem: _cartItems)); }}Copy the code

Note: Replace the CartBloc in the entire code with the CartCubit and trigger the event as shown below.

onPressed: () { ! cart.contains(itemNo) ? BlocProvider.of<CartCubit>(context).add(itemNo) : BlocProvider.of<CartCubit>(context).remove(itemNo); },Copy the code

The output is the same, but the state management is improved.

conclusion

With a solid BLoC architecture, it is possible to separate concerns well. Although using BLoC mode requires more code than using setState, it makes the code more readable, extensible and testable.

In this tutorial, we cover the basics of using BLoC mode in a Flutter and highlight the benefits of using BLoC for state management in a Flutter over using setState with a practical example.

You can find the full source code for this example on GitHub.

The postState management in Flutter using the BLoC design patternappeared first onLogRocket Blog.