27 Jul 2023 Β· Software Engineering

    Understanding State Management in Flutter (Part 3)

    16 min read
    Contents

    BLoC and Redux State Management

    In Flutter, there are various state management patterns available, each with its own strengths and characteristics. Two popular patterns in the Flutter community are the BLoC (Business Logic Component) pattern and the Redux pattern. Both patterns provide solutions for managing state in Flutter applications effectively.

    The BLoC pattern focuses on separating business logic from presentation and allows for reactive and testable code. It employs the use of events, streams, and sinks to handle user actions and update application state accordingly. The BLoC pattern requires a bit more boilerplate code and has a steeper learning curve but offers excellent separation of concerns, responsiveness, testability, and scalability.

    Redux is a well-known state container model that is frequently utilized in web and mobile applications. It ensures unidirectional data flow, making state changes easier to understand and debug. The central store strategy is used by Redux, in which the entire application state is saved in a single immutable object called the store.

    Both the BLoC and Redux patterns have their advantages and use cases. In this article we will look at them individually and understand how they work using flutter applications.

    Business Logic Component (BLoC) Pattern

    BLoC is popular in the Flutter community because of its separation of concerns, responsiveness, testability and scalability. However, it may require more boilerplate code than other state management approaches and has a steeper learning curve.

    There are several core concepts to understand when using BLoC in Flutter:

    • Events: events signify user activities or other actions that can alter the application’s state. Events are typically represented as simple data classes.
    • Bloc: a Bloc is a class that takes in events, processes them, and produces a new state. It is in charge of controlling the application’s state and responding to user input.
    • State: state represents the current state of the application. It is typically represented as an immutable data class.
    • Stream: a stream is a collection of asynchronous events that may be monitored for modifications. In the context of BLoC, Streams are used in BLoC to describe the application’s state at any given time.
    • Sink: a Sink is a Stream controller that can be used to send events to a stream. In the context of BLoC, a Sink is used to send events to the Bloc for processing.
    • StreamController: StreamController is used to construct and manage streams. In the context of BLoC, a StreamController is used to manage the stream(s) of events that are sent to the Bloc.
    • BlocBuilder: BlocBuilder is a widget provided by the flutter_bloc package that helps to connect the Bloc to the user interface. It listens to changes in the state of the Bloc and rebuilds the UI accordingly.
    • BlocProvider: The flutter_bloc package has a widget called BlocProvider that adds a Bloc to the widget tree. It ensures that the Bloc is created only once and is accessible to all the widgets in the subtree.

    These are some of the core concepts that are essential to understanding the BLoC pattern in Flutter. By understanding these concepts, you can create well-architected Flutter applications that are easy to maintain and test.

    BLoC (capitalized) refers to the Business Logic Component pattern, which is a state management pattern while bloc (lowercase) is a term that is often used to refer to an instance of the Bloc class that implements the BLoC pattern.

    So while the two terms are related, they refer to different concepts – BLoC refers to a design pattern, while bloc refers to an instance of a class that implements that pattern. We are going to be using both words so it’s good to recognize the difference.

    Create an application using the BLoC pattern

    We will be creating a simple application of a container that changes its color from red to blue using bloc, events, and states. To use the Bloc pattern for state management, we must add the flutter_bloc package to our project’s dependencies.

    dependencies:
    flutter: 
    sdk: flutter
    flutter_bloc: ^8.1.2

    After adding the package to our project, we are going to create a folder inside our lib folder called bloc, this folder is going to hold our color_bloc.dart file, color_ event.dart file and color_state.dart file, you will create those three files inside the bloc folder.

    Once we have succeeded in creating the folder and files, we are going to proceed to define the events in the Color_event.dart file.

    part of 'color_bloc.dart';
    
    @immutable
    abstract class ColorEvent {}
    
    class InitialEvent extends ColorEvent {
      InitialEvent();
    }
    
    class ColorToBlue extends ColorEvent {
      ColorToBlue();
    }
    
    class ColorToRed extends ColorEvent {
      ColorToRed();
    }

    In this part of the code, we define a set of classes that represent events in a BLoC.

    The first line part of color_bloc.dart'; indicates that the code is part of a larger file called color_bloc.dart which is going to contain the implementation of the BLoC.

    The @immutable annotation is used to indicate that instances of the classes defined below are immutable and cannot be changed once created. This helps to ensure that the state of the application is not accidentally modified.

    The abstract class ColorEvent {}defines an abstract class called ColorEvent that will be used as a base class for all the events in the BLoC. This class does not contain any implementation and cannot be instantiated directly.

    The three classes that follow – InitialEvent, ColorToBlue, and ColorToRed – are concrete classes that extend the ColorEvent class. They represent different types of events that can be sent to the BLoC to trigger a state change.

    The InitialEvent class represents the initial event that is sent to the BLoC when it is first created. It is typically used to initialize the state of the application.

    The ColorToBlue and ColorToRed classes represent events that can be sent to the BLoC to change the color state of the application. They do not contain any additional information beyond the fact that the color should be changed to blue or red, respectively.

    These classes define the events that can be sent to the BLoC and help to ensure that the state of the application is updated in a predictable and controlled way.

    Let us define the states that the Bloc will handle.

    part of 'color_bloc.dart';
    
    @immutable
    abstract class ColorState {}
    
    class ColorInitial extends ColorState {}
    
    class ColorUpdateState extends ColorState {
      bool? initialState;
     
      ColorUpdateState({
        this.initialState,
      });
    }

    This code defines a set of classes that represent the state of the bloc. We have already explained part of β€˜color_bloc.dart and @immutable.

    The abstract class ColorState {} defines an abstract class called ColorState that will be used as a base class for all the states in the bloc. This class does not contain any implementation and cannot be instantiated directly.

    The two classes that follow – ColorInitial and ColorUpdateState – are concrete classes that extend the ColorState class. They represent different states that the bloc will be in.

    The ColorInitial class represents the initial state of the bloc. It is used to indicate that the bloc has just been created and has not yet received any events.

    The ColorUpdateState class represents a state where the color of the application has been updated. It contains a boolean value called initialState that indicates the current state of the color.

    The ColorUpdateState class also has a named constructor that takes an optional initialState parameter. This constructor can be used to create new instances of the ColorUpdateState class with a specified initialState value.

    These classes define the different states that the bloc can be in and help to ensure that the state of the application is updated in a predictable and controlled way.

    We then define a Bloc class that handles the events and updates the state of the application.

    import 'package:bloc/bloc.dart';
    import 'package:meta/meta.dart';
    
    part 'color_event.dart';
    part 'color_state.dart';
    
    class ColorBloc extends Bloc<ColorEvent, ColorState> {
      bool initState = true;
      ColorBloc() : super(ColorInitial()) {
        on<ColorEvent>((event, emit) {
          //Implement an event handler
        });
        on<InitialEvent>((event, emit) {
          //  implement event handler
          emit(ColorUpdateState(initialState: initState));
        });
        on<ColorToBlue>((event, emit) {
           //implement event handler
          initState = true;
          emit(ColorUpdateState(initialState: initState));
        });
        on<ColorToRed>((event, emit) {
          initState = false;
          emit(ColorUpdateState(initialState: initState));
        });
      }
    }

    The ColorBloc class takes two type parameters, ColorEvent and ColorState, which represent the events that can be sent to the bloc and the states that can be emitted by the bloc, respectively.

    We also imported the meta package and defined two-part directives that import the ColorEvent and ColorState classes from their separate files.

    In the constructor of the ColorBloc class, the super keyword is used to call the constructor of the Bloc class and initialize the initial state of the bloc to ColorInitial().

    The on method is used to register event handlers for different types of events. For example, when a ColorToBlue event is received, the ColorUpdateState state is emitted with the initialState field set to true. 

    Similarly, when a ColorToRed event is received, the ColorUpdateState state is emitted with the initialState field set to false.

    We are using the initState boolean variable to keep track of the current state of the bloc. When the ColorToBlue event is received, the initState variable is set to true, and when the ColorToRed event is received, the initState variable is set to false.

    So, overall, what we just did is define a ColorBloc class that can handle different types of events and emit corresponding states based on the current state of the bloc.

    To use ColorBloc in the widget tree, we must wrap it with the BlocProvider widget.

    lass MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
    //Wrap with the BlocProvider widget
          home:
              BlocProvider(create: (_) => ColorBloc(), child: const ColorScreen()),
        );
      }
    }

    In the previous code, we wrapped the MaterialApp widget with the BlocProvider widget.

    BlocProvider is a widget provided by the bloc package. It is used to provide a Bloc instance to a subtree of widgets, making it available to be used by all widgets in that subtree.

    We have also provided the CounterBloc instance to the create parameter of the BlocProvider widget.

    It’s time to manage the state of our app using the states and events created.

    class ColorScreen extends StatefulWidget {
      const ColorScreen({super.key});
    
      @override
      State<ColorScreen> createState() => _ColorScreenState();
    }
    
    class _ColorScreenState extends State<ColorScreen> {
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
       
        context.read<ColorBloc>().add(InitialEvent());
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white,
          appBar: AppBar(
            title: const Text('colorz'),
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              BlocConsumer<ColorBloc, ColorState>(listener: (context, state) {
                print(state);
              }, builder: (context, state) {
                if (state is ColorUpdateState) {
                  return Column(
                    children: [
                      Container(
                        width: 200,
                        height: 200,
                        color: state.initialState == true ? Colors.blue : Colors.red,
                      ),
                      SizedBox(
                        height: 20,
                      ),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          GestureDetector(
                            onTap: () =>
                                context.read<ColorBloc>().add(ColorToBlue()),
                            child: Container(
                              width: 50,
                              height: 30,
                              decoration: BoxDecoration(
                                color: Colors.blue,
                              ),
                              child: Center(child: Text('blue')),
                            ),
                          ),
                          const SizedBox(
                            width: 20,
                          ),
                          GestureDetector(
                            onTap: () =>
                                context.read<ColorBloc>().add(ColorToRed()),
                            child: Container(
                              width: 50,
                              height: 30,
                              decoration: BoxDecoration(
                                color: Colors.red,
                              ),
                              child: Center(child: Text('red')),
                            ),
                          ),
                        ],
                      ),
                    ],
                  );
                } else {
                  return Container();
                }
              }),
            ],
          ),
        );
      }
    }

    In this code, We are using the BlocConsumer widget from the flutter_bloc library to consume the state changes emitted by a ColorBloc instance in ColorScreen widget .

    The BlocConsumer widget has two builder functions: a builder function and a listener function.

    The builder function is a function responsible for creating the user interface (UI) based on the current state of the BLoC. On the other hand, the listener function is called whenever a new state is emitted by the BLoC, allowing the UI to be updated accordingly.

    To put it simply, the builder function builds the initial UI, while the listener function is responsible for updating the UI whenever there is a change in the state of the BLoC.

    This helps keep the UI in sync with the underlying data and provides a clear separation of concerns between the presentation and business logic.

    The builder function takes in the context and the current state of the ColorBloc as arguments. If the current state is an instance of ColorUpdateState, the UI is built with a Container widget that displays a colored box based on the value of the initialState field of the ColorUpdateState instance. So, if the initialState is true, the box is colored blue, otherwise it is colored red.

    We also have two GestureDetector widgets that allow the user to change the color of the box by tapping on them. Tapping on the blue GestureDetector widget dispatches a ColorToBlue event to the ColorBloc, while tapping on the red GestureDetector widget dispatches a ColorToRed event.

    The listener function in this code simply prints the new state every time a state change occurs in the ColorBloc. Finally, in the initState method of the _ColorScreenState class, a new InitialEvent is dispatched to the ColorBloc using the context.read().add(InitialEvent()) method call. This initializes the state of the ColorBloc with the default state specified in the constructor of the ColorBloc class.

    Full BloC code here

    Redux pattern

    In Flutter, the Redux pattern can be implemented using the flutter_redux package. The package offers a Store class that allows the creation of a store for the application.

    Furthermore, it includes a middleware feature that enables the interception and alteration of actions before they are processed by reducers.

    Create an application that uses the Redux pattern

    For us to better understand the Redux pattern, let’s create an app that also increases and decreases when a button is tapped but using the Redux package.

    To use the Redux pattern in your Flutter application, you need to follow a few steps:

    • Define the State: define the state of your application as a class that extends the Equatable class from the equatable package. Equatable is used to compare objects for equality, which is important for performance optimization.
    import 'package:equatable/equatable.dart';
    
    
    class CounterState extends Equatable {
      final int count;
    
      CounterState({this.count = 0});
    
      CounterState copyWith({int count}) {
        return CounterState(count: count ?? this.count);
      }
    
      @override
      List<Object> get props => [count];
    }
    • Define the Actions: define the actions that can be dispatched to the store as classes. These actions should be immutable and contain all the information required to update the state.
    class IncrementAction {}
    class DecrementAction {}
    • Define the Reducers: define reducers that take the current state and an action as input, and return a new state as output. Reducers should be pure functions, meaning that they should not have any side effects.
    CounterState counterReducer(CounterState state, dynamic action) {
      if (action is IncrementAction) {
        return state.copyWith(count: state.count + 1);
      } else if (action is DecrementAction) {
        return state.copyWith(count: state.count - 1);
      }
    
      return state;
    }
    • Create the Store: create the store for your application using the Store class from the flutter_redux package. Pass the reducer function and initial state to the constructor of the Store class.
    import 'package:flutter_redux/flutter_redux.dart';
    import 'package:redux/redux.dart';
    import 'package:flutter/material.dart';
    import 'package:redux_state_management/redux/actions.dart';
    import 'redux/app_state.dart';
    import 'redux/reducers.dart';
    
    
    void main() {
      final store = Store<CounterState>(
        counterReducer,
        initialState: CounterState(),
      );
    
      runApp(MyApp(store));
    }
    
    class MyApp extends StatelessWidget {
      final Store<CounterState> store;
    
      MyApp(this.store);
    
      @override
      Widget build(BuildContext context) {
        return StoreProvider<CounterState>(
          store: store,
          child: MaterialApp(
            title: 'Flutter Redux example',
            home: CounterScreen(),
          ),
        );
      }
    }
    • Dispatch Actions: dispatch actions to the store using the dispatch method of the Store class.

    In the following code, we dispatch the IncrementAction and DecrementAction actions when the respective buttons are pressed.

    class CounterScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Flutter Redux Demo'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                StoreConnector<CounterState, int>(
                  converter: (store) => store.state.count,
                  builder: (context, count) => Text(
                    count.toString(),
                    style: TextStyle(fontSize: 48),
                  ),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    StoreConnector<CounterState, VoidCallback>(
                      converter: (store) => () => store.dispatch(IncrementAction()),
                      builder: (context, callback) => ElevatedButton(
                        onPressed: callback,
                        child: Text('Increment'),
                      ),
                    ),
                    SizedBox(width: 16),
                    StoreConnector<CounterState, VoidCallback>(
                      converter: (store) => () => store.dispatch(DecrementAction()),
                      builder: (context, callback) => ElevatedButton(
                        onPressed: callback,
                        child: Text('Decrement'),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }

    In the above code, we use the StoreConnector widget from the flutter_redux package to connect our widget to the store.

    We used the converter parameter to convert the state of the store to a value that can be used by the widget. In this case, we convert the count field of the CounterState class to an integer.

    The builder parameter builds the widget using the converted value. We also used the two StoreConnector widgets to connect the two buttons to the store.

    The converter parameter is used to create a callback function that dispatches the respective action to the store when the button is pressed.

    Full redux code here

    By following these steps, you can implement the Redux pattern for state management in your Flutter application. The Redux pattern is a great tool for managing the state of your application, especially for large and complex applications.

    Conclusion

    The use of BLoC and Redux depends on factors such as the size and complexity of your project, the team’s familiarity with the patterns, and personal preferences. Both BLoC and Redux have vibrant ecosystems with extensive community support and numerous packages available. By leveraging the power of these state management patterns, you can build robust and scalable Flutter applications with ease.

    Thank you for reading Part 3 of this series on understanding state management in Flutter. We hope you found the information on BLoC and Redux useful .

    If you’re interested in diving deeper into this topic, check out parts one and two.

    In Part 1, we explore the stateful widget and what you should consider when choosing a state management technique. We discuss the lifecycle of a stateful widget, setState and we use the stateful widget to build a counter app . You can read it here: Understanding State Management in Flutter (Part 1) .

    In Part 2, we explore two popular state management approaches called Provider and InheritedWidget. We discuss their features, benefits, and we also use the state management techniques to create applications. You can read it here: Understanding State Management in Flutter (Part 2) .

    Resources for Further Learning:

    Redux Documentation: https://pub.dev/packages/flutter_redux

    Flutter BLoC package: https://pub.dev/packages/flutter_bloc

    Leave a Reply

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

    Avatar
    Writen by:
    I'm a mobile developer with a passion for creating pixel-perfect products and interactive user experiences. Additionally, I excel in technical writing, breaking down complex topics into easily understandable bits for individuals at any level.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.