DEV Community

Hany Sameh
Hany Sameh

Posted on

Flutter Bloc

What is flutter bloc?

Flutter bloc is one of the state management for Flutter applications. You can use it to handle all the possible states of your application in an easy way.

https://user-images.githubusercontent.com/38296077/126981491-e13d63ec-932d-4640-a9c2-927259ce48a7.jpg

Why Bloc?

Bloc makes it easy to separate presentation from business logic, making your code fasteasy to test, and reusable.

Advantages

  • Understand easily what’s happening inside your app.
  • More structured code, Easily to maintain an test.
  • Know and understand every state of your app.
  • Work on a single, stable, popular and effective bloc codebase.

How does it work?

When you use flutter bloc you are going to create events to trigger the interactions with the app and then the bloc in charge is going to emit the requested data with a state

https://miro.medium.com/max/720/0*EG9GW70kfdesSPRZ.webp

Core Concepts of Bloc

  • Streams

    💡 **A stream is a sequence of asynchronous data.**

    In order to use the bloc library, it is critical to have a basic understanding of Streamsand how they work.

    If you're unfamiliar with Streams just think of a pipe with water flowing through it. The pipe is the Stream and the water is the asynchronous data.

    We can create a  Stream  in Dart by writing an async* (async generator) function.

    Stream<int> countStream(int max) async* {
        for (int i = 0; i < max; i++) {
            yield i;
        }
    }
    

    By marking a function as async* we are able to use the yield keyword and return a Stream of data. In the above example, we are returning a  Stream of integers up to the max integer parameter.

    Every time we yield in an async* function we are pushing that piece of data through the Stream .

    We can consume the above Stream in several ways. If we wanted to write a function to return the sum of a Stream of integers it could look something like:

    Future<int> sumStream(Stream<int> stream) async {
        int sum = 0;
        await for (int value in stream) {
            sum += value;
        }
        return sum;
    }
    

    By marking the above function as async we are able to use the await keyword and return a Future of integers. In this example, we are awaiting each value in the stream and returning the sum of all integers in the stream.

    We can put it all together like so:

    void main() async {
        /// Initialize a stream of integers 0-9
        Stream<int> stream = countStream(10);
        /// Compute the sum of the stream of integers
        int sum = await sumStream(stream);
        /// Print the sum
        print(sum); // 45
    }
    

    Now that we have a basic understanding of how Streams work in Dart we're ready to learn about the core component of the bloc package: a Cubit .

  • Cubit

    💡 **A `Cubit` is a class which extends `BlocBase` and can be extended to manage any type of state.**

    Cubit can expose functions which can be invoked to trigger state changes.

    https://bloclibrary.dev/assets/cubit_architecture_full.png

    States are the output of a Cubit and represent a part of your application's state. UI components can be notified of states and redraw portions of themselves based on the current state.

    • Create A Cubit:

      We can create a CounterCubit like:

      class CounterCubit extends Cubit<int> {
        CounterCubit() : super(0);
      }
      

      When creating a Cubit, we need to define the type of state which the Cubit will be managing. In the case of the CounterCubit above, the state can be represented via an int but in more complex cases it might be necessary to use a classinstead of a primitive type.

      The second thing we need to do when creating a Cubit is specify the initial state. We can do this by calling super with the value of the initial state. In the snippet above, we are setting the initial state to 0 internally but we can also allow the Cubit to be more flexible by accepting an external value:

      class CounterCubit extends Cubit<int> {
        CounterCubit(int initialState) : super(initialState);
      }
      

      This would allow us to instantiate CounterCubitinstances with different initial states like:

      final cubitA = CounterCubit(0); // state starts at 0
      final cubitB = CounterCubit(10); // state starts at 10
      
    • State Changes:

      💡 **Each `Cubit` has the ability to output a new state via `emit`.**
      class CounterCubit extends Cubit<int> {
        CounterCubit() : super(0);
      
        void increment() => emit(state + 1);
      }
      

      In the above snippet, the CounterCubit is exposing a public method called increment
      which can be called externally to notify the CounterCubit to increment its state. When increment is called, we can access the current state of the Cubit via the state
      getter and emit a new state by adding 1 to the current state.

      Note:

      The emit method is protected, meaning it should only be used inside of a Cubit.

  • Bloc

    💡 Bloc is a more advanced class which relies on events to trigger state changes rather than functions. Bloc also extends BlocBase which means it has a similar public API as Cubit. However, rather than calling a function on a Bloc and directly emitting a new stateBlocs receive events and convert the incoming events into outgoing states.

    • Create A Bloc:

      Creating a Bloc is similar to creating a Cubit except in addition to defining the state that we'll be managing, we must also define the event that the Bloc will be able to process.

      💡 **Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.**
      abstract class CounterEvent {}
      
      class CounterIncrementPressed extends CounterEvent {}
      
      class CounterBloc extends Bloc<CounterEvent, int> {
        CounterBloc() : super(0);
      }
      

      Just like when creating the CounterCubit, we must specify an initial state by passing it to the superclass via super.

    • State Changes:

      Bloc requires us to register event handlers via the on<Event> API, as opposed to functions in Cubit.

      An event handler is responsible for converting any incoming events into zero or more outgoing states.

      abstract class CounterEvent {}
      
      class CounterIncrementPressed extends CounterEvent {}
      
      class CounterBloc extends Bloc<CounterEvent, int> {
        CounterBloc() : super(0) {
          on<CounterIncrementPressed>((event, emit) {
            // handle incoming `CounterIncrementPressed` event
          })
        }
      }
      
      💡 **Tip**: an **`EventHandler`** has access to the added event as well as an **`Emitter`** which can be used to emit zero or more states in response to the incoming event.

      We can then update the EventHandler to handle the CounterIncrementPressed event:

      abstract class CounterEvent {}
      
      class CounterIncrementPressed extends CounterEvent {}
      
      class CounterBloc extends Bloc<CounterEvent, int> {
        CounterBloc() : super(0) {
          on<CounterIncrementPressed>((event, emit) {
            emit(state + 1);
          });
        }
      }
      

      In the above snippet, we have registered an EventHandler to manage all  CounterIncrementPressed events. For each incoming CounterIncrementPressed event we can access the current state of the bloc via the state getter and emit(state + 1).

Bloc vs Cubit

Screen Shot 2023-02-01 at 7.39.54 PM.png

  • Cubit

    Cubit is a subset of the BLoC Pattern package that does not rely on events and instead uses methods to emit new states.

    So, we can use Cubit for simple states, and as needed we can use the Bloc.

    There are many advantages of choosing Cubit over Bloc. The two main benefits are:

    Cubit is a subset of Bloc; so, it reduces complexity. Cubit eliminates the event classes. Cubit uses emit rather than yield to emit state. Since emit works synchronously, you can ensure that the state is updated in the next line.

    note:

    • We don’t have streams, so no transformations.

    Screen Shot 2023-02-01 at 7.48.48 PM.png

  • Bloc

    When we use BLoC, there’s two important things: “Events” and “States”. That means that when we send an “Event” we can receive one or more “States”, and these “States” are sent in a stream

    • We have two streams here: Events and States; and that means that all things that we can apply to a Stream we can do it here. Transformations.
    • We can do long duration operations here, things like: API calls, database, compressions or some other complex things.

    notes:

    • If you send a lot of events for one BLoC could be difficult to track if you are not tracking them correctly.
    • Sometimes we want to receive this “state” synchronously, but with BLoC this is not possible.

    Screen Shot 2023-02-01 at 7.54.25 PM.png

Note: 
BLoC uses mapToState and gives you a stream (async*). Cubit uses normal functions and gives you the result immediately or a Future (with async).

Screen Shot 2023-02-01 at 7.52.41 PM.png

Flutter Bloc Concepts

BlocProvider

Screen Shot 2023-02-02 at 12.37.37 AM.png

💡 Is a flutter widget which creates and provides a Bloc to all of its children.
  • Is also known as as dependency injection.
  • BlocProvider will provide a single instance of a Bloc to the subtree below it.

    Screen Shot 2023-02-02 at 12.43.04 AM.png

// you must add flutter_bloc package dependency in pubspec.yaml
BlocProvider(
    create: (BuildContext context) => BlocA(),
    child: yourWidget(),
);

// The context in which a specific widget is built

Enter fullscreen mode Exit fullscreen mode

we access it in a subtree like:

BlocProvider.of<BlocA>(context);

// OR

context.read<BlocA>();
Enter fullscreen mode Exit fullscreen mode
  • By default, BlocProvider create the Bloc lazily

    meaning that the create function will get executed when the Bloc is looked up for via BlocProvider of Bloc a context

    note:

    To override this behavior and force the BlocProvider create function to be run immediately as soon as possible laze can be set to false inside the widget’s parameters like:

    BlocProvider(
        create: (BuildContext context) => BlocA(),
        laze: false,
        child: yourWidget(),
    ); 
    
  • BlocProvider handles the closing part of Bloc automatically so that where won’t be any stream leaks all over the place

Question

When we push a new screen into the navigation routing feature of flutter is the BlocA going to be available there too?

No, because there is another context created on the new rout, a context which the BlocProvider doesn’t know yet.

so in order to pass the only instance of the Bloc to the new rout, we will use BlocProvider.value()

Screen Shot 2023-02-02 at 1.07.29 AM.png

BlocProvider.value(
    value: BlocProvider.of<BlocA>(context),
    child: newChild(),
);
Enter fullscreen mode Exit fullscreen mode

Example:

build counter app with bloc

// in this app we use Cubit
// 1- create app states -> counter_states.dart
class CounterState{
int counterValue;
CounterState({
required this.counterValue,
});
}

// 2- create cubit -> counter_cubit.dart
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState(counterValue:0));

  void increment() => emit(CounterState(counterValue:state.counterValue + 1));

  void decrement() => emit(CounterState(counterValue:state.counterValue - 1));
}

// Ui 
class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(), // -> create counter cubit
      child: MaterialApp(
                body:counterScreen(),
            ),
    );
  }
}

//in counter screen you access increment and decrement methods in buttons like:

FloatingActionButton(
    child: const Icon(Icons.add),
    onPressed: () => context.read<CounterCubit>().increment(),//access increment
  ),

FloatingActionButton(
    child: const Icon(Icons.remove),
    onPressed: () => context.read<CounterCubit>().decrement(),//access decrement
  ),
Enter fullscreen mode Exit fullscreen mode

The Question now is How do we receive the new state inside the UI ?, How do we rebuild specific text widget which prints out the counter value ?

It is now time to introduce the second important Flutter Bloc concept which is BlobBuilder

BlocBuilder

💡 A widget that helps Re-Building the UI based on Bloc state changes

Re-Building a large chunk of the UI required a lot of time to compute, good practice would be to wrap the exact part of the UI you want to rebuild inside BlocBuilder

Screen Shot 2023-02-02 at 3.29.29 AM.png

Ex:

If you have a text widget which updates from a sequence of emitted states and that text is inside a tone of Columns, Rows and other widgets , it is a huge mistake to rebuild all of them just to update the text widget

to rebuild only the text widget → wrap it inside BlocBuilder

  • BlocBuilder is a widget which requires a Bloc or Cubit and the builder function

    builder function → will potentially be called many time due to how flutter engine works behind the scenes and should be a pure function that returns a widget in response to a state.

    Pure Function → is a function return value depends only on the function’s arguments and no others.

    Screen Shot 2023-02-02 at 3.40.14 AM.png

    So in this case our builder function should return a widget which only depend on the context and state parameters .

    BlocBuilder<BlocA,BlocAStates>(
        builder: (context, state){
            return widget;
        }
    )
    

    If the Bloc or Cubit is not provided the BlocBuilder will automatically perform lookup for its instance using BlocProvider and the current build context.

    • you can use buildWhen parameter this should take in a function having the previous state and current state as parameters

      Ex:

      If text value in current state greater than the text value in previous state you can return false so that the Builder function won’t trigger and won’t rebuild UI

      BlocBuilder<BlocA,BlocAStates>(
          builder: (context, state){
              return widget;
          }
          buildWhen: (previous, current){}
      )
      

BlocListener

has mainly the same structure as BlocBuilder but it is really deferent in many ways

💡 Is a flutter widget which listens to any sate change as BlocBuilder does but instead of rebuilding a widget as BlocBuilder did with the builder function it takes a simple void function called listener which is called only once per state not including the initial state.

There is also a listenWhen parameter function you can pass to tell the BlocListener when to call the listener function or not as build when parameter was for the BlocBuilder widget.

BlocListener<BlocA,BlocAStates>(
    listener: (context, state){
        debugPrint("do stuff have on BlocA's state")
    },
    child: widget,
)
Enter fullscreen mode Exit fullscreen mode

🔄  In our Counter App:

We need to Re-Build text widget and print increment when click add button and decrement when click remove button, Let’s do this

// rebuild text widget 
BlocBuilder<CounterCubit, CounterState>(
       builder: (context, state) {
           return Text(
              state.counterValue.toString(), // initial value = 0
                  style: Theme.of(context).textTheme.headlineLarge,
      );
   },
),

// to print in listener we need ro refactor CounterState and CounterCubit
// Let's do this
// refactor CounterState to add wasIncremented variable
class CounterState {
  int counterValue;
  bool? wasIncremented;
  CounterState({
    required this.counterValue,
    this.wasIncremented,
  });
} 

// refactor Cubit to change wasIncremented variable value on emit state 
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState(counterValue: 0));

  void increment() => emit(
        CounterState(
          counterValue: state.counterValue + 1,
          wasIncremented: true, // when click add button  
        ),
      );

  void decrement() => emit(
        CounterState(
          counterValue: state.counterValue - 1,
          wasIncremented: false, // when click remove button
        ),
      );
}

// Now you can access this variable when listen to state change 
    Scaffold(
        body: BlocListener<CounterCubit, CounterState>(
        listener: (context, state) {
          if (state.wasIncremented == true) {
            debugPrint('Incremented');
          } else {
            debugPrint('Decremented');
          }
        },
        child: CounterDesign(),
);

// Now We are done 
// Now you have created your first Flutter App with Bloc
Enter fullscreen mode Exit fullscreen mode

Perhaps you are in our situation right now when you are updating UI using BlocBuilder and also print something using BlocListener isn’t there an easier way to do it of course there is, it is called BlocConsumer

BlocConsumer

💡 Is a widget which combines both BlocListener and BlocBuilder into a single widget

Screen Shot 2023-02-02 at 4.51.42 AM.png

BlocSelector

💡 Is a Flutter widget which is analogous to BlocBuilder but allows developers to filter updates by selecting a new value based on the current bloc state. Unnecessary builds are prevented if the selected value does not change. The selected value must be immutable in order for BlocSelectorto accurately determine whether buildershould be called again.

If the blocparameter is omitted, BlocSelectorwill automatically perform a lookup using  BlocProvider and the current BuildContext .

BlocSelector<BlocA, BlocAState, SelectedState>(
  selector: (state) {
    // return selected state based on the provided state.
  },
  builder: (context, state) {
    // return widget here based on the selected state.
  },
)
Enter fullscreen mode Exit fullscreen mode

MultiBlocProvider

💡 Is a Flutter widget that merges multiple BlocProviderwidgets into one.

MultiBlocProvider improves the readability and eliminates the need to nest multiple  BlocProviders.

Example without it:

BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

Example with it:

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)
Enter fullscreen mode Exit fullscreen mode

MultiBlocListener

💡 Is a Flutter widget that merges multiple BlocListener widgets into one

MultiBlocListener improves the readability and eliminates the need to nest multiple  BlocListeners .

// without it
BlocListener<BlocA, BlocAState>(
  listener: (context, state) {},
  child: BlocListener<BlocB, BlocBState>(
    listener: (context, state) {},
    child: BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
      child: ChildA(),
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

→ with MultiBlocListener

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
)
Enter fullscreen mode Exit fullscreen mode

RepositoryProvider

💡 Is a Flutter widget which provides a repository to its children via  RepositoryProvider.of(context)

The same exact widget as a BlocProvider the only difference being that it provides a single repository class instance instead of a single Bloc Instance.

⇒ It is used as a dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree

NOTE:

BlocProvider should be used to provide blocs whereas RepositoryProvider should only be used for repositories.

RepositoryProvider(
  create: (context) => RepositoryA(),
  child: ChildA(),
);
Enter fullscreen mode Exit fullscreen mode

then from ChildA we can retrieve the Repository instance with:

// with extensions
context.read<RepositoryA>();

// without extensions
RepositoryProvider.of<RepositoryA>(context)
Enter fullscreen mode Exit fullscreen mode

MultiRepositoryProvider

💡 is a Flutter widget that merges multiple RepositoryProvider widgets into one.

MultiRepositoryProvider improves the readability and eliminates the need to nest multiple RepositoryProvider .

By using MultiRepositoryProvider we can go from:

RepositoryProvider<RepositoryA>(
  create: (context) => RepositoryA(),
  child: RepositoryProvider<RepositoryB>(
    create: (context) => RepositoryB(),
    child: RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
      child: ChildA(),
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

To:

MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)
Enter fullscreen mode Exit fullscreen mode

BlocObserver

One added bonus of using the bloc library is that we can have access to all Changes in one place. Even though in this application we only have one Cubit, it's fairly common in larger applications to have many Cubits managing different parts of the application's state.

If we want to be able to do something in response to all Changes we can simply create our own BlocObserver.

class SimpleBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }
}
Enter fullscreen mode Exit fullscreen mode

In order to use the SimpleBlocObserver, we just need to tweak the main function:

void main() {
  Bloc.observer = SimpleBlocObserver();
  CounterCubit()
    ..increment()
    ..close();  
}
Enter fullscreen mode Exit fullscreen mode

The above snippet would then output:

Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }
Enter fullscreen mode Exit fullscreen mode

You can observe a Cubit or Bloc Like:

// ObserveACubit
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }
}

// ObserveABloc
abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }
Enter fullscreen mode Exit fullscreen mode

Bloc Architecture

First we ask the questions that are on your mind

What is an Architecture?

What is all about?

Why we need an ArchItecture?

Let’s start

We as human species can’t live without a predefined and stable architecture which is mainly our skeleton, imagine having all kinds of different classes, methods, functions, variables all over the place, this is definitely going to result in a total failure.

Now assimilate this to us humans, How would our life be if we only had our organs every one had to choose where to place their heart, their lungs, their kidneys as probably none of these people would have survived in this situation without organized structure and stable skeleton neither would have any of the apps built without a proper architecture, it’s as simple as that.

Screen Shot 2023-02-03 at 12.57.33 AM.png

so think of architecture as being the skeleton, the blueprint, the structure which keep all your code organized stable and easy to test and maintain.

Now Bloc Architecture is simply an architecture which has Bloc as it center of gravity.

Screen Shot 2023-02-03 at 1.01.47 AM.png

So not only is Bloc a Design Pattern and State Management library but it also an Architecture Pattern

Screen Shot 2023-02-03 at 1.03.40 AM.png

What we know for now is that for every interaction an user makes with the app through each UI there should be an event dispatched to the specialized Bloc or Cubit which will process it and will eventually emit a state that is going to rebuild the UI in a way so that the user gets a feedback of what’s going on with the app, The big unknown variable in all this equation is how Bloc processes the event and perhaps retrieves the necessary data to show to the user, Let me put it this for you almost every app now a days retrieves its data from the internet, so in order to link our Bloc based flutter app with the outer data layer, we need to add the data layer into our equation so that for example:

when Bloc receives a fetching event from UI it will request some data from internet, retrieve it as a response, parse it and return the data with a new state to the user.

Let’s talk about layers

We can split this into three separated main layers which not surprisingly will depend one on another, so the UI is mostly seen as a Presentation Layer , The Bloc/Cubit as Business Logic Layer and the last but not the least the app’s data as simply the Data Layer

Screen Shot 2023-02-03 at 1.23.46 AM.png

We’re going to start with Data Layer which is farthest from the user interface and work our way up to the Presentation Layer

Data Layer

💡 has Responsibility of retrieving and also manipulating data from one or more sources whether we’re talking about network, Database, Request or other asynchronous sources.

Screen Shot 2023-02-03 at 1.28.37 AM.png

The Date Layer has been split up inside three important sub-layers :

  • Models
  • Data Provider
  • Repositories

They will also be dependent on one another.

Screen Shot 2023-02-03 at 1.30.44 AM.png

Suppose you want to build your own first app and you don’t know where to actually start programming the app, what is actually the first thing you should code ?

The best way to start your app of course after you visually and technically designed it is by coding your models, but what exactly is a model?

⇒ Let’s start with model

Model

A model is as its name is implying a blueprint to the data your application will work with.

Screen Shot 2023-02-03 at 1.36.58 AM.png

is a nothing more than a class which contains the data the application itself will be dependent on.

Example → weather API

Screen Shot 2023-02-03 at 1.41.24 AM.png

they shouldn’t be identical to the data provider, Why?

think of what is going to happen if you have weather apis in your app as a source to your weather data their apis will set completely different jsons to your app perhaps on of them has the temperature inside a temp name field and the other one a temperature name field then you will need to parse them separately to your universal models, remember that inside many apps might be multiple data sources but it’s recommended to have only one special data modelin which they will parsed.

Screen Shot 2023-02-03 at 1.44.02 AM.png

This means that models should be independent enough to get weather data from many different weather APIs.

Screen Shot 2023-02-03 at 1.52.31 AM.png

So now after you hopefully understood what a model is, it is time to move over to next sub-layer the Data Provider

Data Provider

💡 Data Provider’s responsibility is to provide raw data to its successor the repository sub-layer

⏰  Let’s concentrate on the Data Provider for a moment..

A Data Provider is actually an API for you own app this means that it is going to be a class which contain different methods and these methods will serve as a direct communication way with the data sources

this where all the magic happens, this is where flutter asks for the required data straight from the internet, all your network requests like http.get , [http.post](http://http.post) , put , delete will go inside take in mind that the return type of these methods won’t to be the type of model you created earlier but rather of the type of row data you received from the data source which for example may be a type of json string

Screen Shot 2023-02-03 at 2.10.48 AM.png

the component in which we’ll have our model class is instantiated as objects is repository

Repository

💡 Is mainly a wrapper around one ore more Data Providers, it’s safe to say that it’s the part of data layer

Bloc communicates with similar to the other data layer sub parts

  • Repositories are also classes which contain dependencies of their respective data providers so for example:

    the weather repository will have a dependency on the weather provider with help of which we’ll call the getRawWeather method and retrieve the weather RowData .

    Screen Shot 2023-02-03 at 2.26.08 AM.png

  • Repository is where the model object will be instantiated with the RowData from the DataProvider or raw data which will be model data with fromJson methods.

  • The Repository is also a perfect place for Fine-Tuning the data before giving it as a response to the Bloc, here you can filter it, sort it and do all kinds of last of last moment changes before it will be sent to the Business Logic Layer .

Business Logic Layer

💡 Is where Bloc and Cubit will be created inside your flutter app, its main responsibility is to respond to user input from the Presentation layer with new emitted state.

  • This layer is meant to be the mediator between the beautiful bright and colorful side of the UI and the dark dangerous and unstable side of the Data Layer

    Screen Shot 2023-02-03 at 2.31.30 AM.png

  • The Business Logic Layer is the last layer that can intercept and catch any errors from with in the Data Layer and protect the application from crashing.

Now since this layer is closely related to the data layer especially the repository sub-layer it can depend on one or more repositories to retrieve the data needed to build up the app state.

Screen Shot 2023-02-03 at 2.38.32 AM.png

🟢  Important fact:

You need to know and understand is that Bloc can communicate one with each other, Cubit can do this too, this is really important

So let’s say we have our previous weather Bloc and we also have an InternetBloc which emits states based on weather there is a stable internet connection or not

Supposedly your internet connection dies when you want to know the weather from your location inside the WeatherBloc, you can depend on the InternetBloc and subscribe to its stream of emitted states then react on every internet state emitted by the InternetBloc

Screen Shot 2023-02-03 at 2.51.11 AM.png

So in this case the InternetBloc would have emitted a no internet state down the stream the weather Bloc will listen to the stream and will eventually receive the no internet state, the WeatherBloc can also emit a state in response to this internet state letting the user know that there is no internet connection.

🚫  Don’t Forget :]

⇒ The subscription to the Bloc needs to be closed manually by overriding the close method, We don’t any stream leaks inside our app

🎉 We have arrived at our final layer of the Bloc Architecture the Presentation Layer

Presentation Layer

This layer sums up every thing related to the user inter face like Widgets, User Input, Lifecycle events, animations and so on and so froth, also its responsibility in our case is to figure out how to render itself based on one or more Bloc State.

⇒ Most application flows will start with perhaps an app start event which triggers the app to fetch some data from the Data Layer

For Example:

When the first screen of the app will be created inside this constructor there will be a WeatherBloc which adds the app started event so that some location based weather will be displayed on the screen.

Screen Shot 2023-02-03 at 3.31.36 AM.png

Great, Now we are finished all app layers 🎉

→ final structure of weather app

Screen Shot 2023-02-03 at 3.41.05 AM.png

Let’s talk about anther important topic in Bloc, which is BlocTesting

Bloc Testing

💡 Bloc was designed to be extremely easy to test.

Why do developers hate testing so much?

The answer is so simple, We hate testing because the application works fine without it, it seems like a reasonable answer right and indeed it is an application can work without testing but doesn’t mean we should disconsider it so badly.

Testing is in so many cases a life saver, it’s the hero nobody talks about on the news

so if you hate testing much, think of it as acute little dog which will be loyal to you and your app all the time, think of it as a layer which will bark whenever something breaks inside your app

Screen Shot 2023-02-05 at 3.01.19 AM.png

Now let me ask you something, have you ever worked on a big project in which wanted to add anew feature and you realize that after 2 or even 3 days of work despite the fact that the feature works perfectly fine it breaks other features in a completely different place inside your app or maybe have you thought if you ever want to delete a piece of code from within your app will this action be destructive or non-destructive to the app itself, How would you check if every thing is ok ?

after wards most of you will say that they will run the app and check every feature to see if every thing works well but i’m sure that some of you won’t even physically test it and live with the idea that nothing will break a conclusion which is fundamentally wrong.

Screen Shot 2023-02-05 at 3.02.58 AM.png

⇒ Keep that in mind.

So what if i tell you that there is something which will run a full checkup of every feature you wrote inside your app so that whenever you refactor your code

Screen Shot 2023-02-05 at 3.11.07 AM.png

you’ll be stress-free without having to worry about if it’s gonna crash or not, you have already guessed that, i’m talking about our body testing.

Screen Shot 2023-02-05 at 3.13.09 AM.png

Now the difference between Pros & Cons → becomes a problem of

Screen Shot 2023-02-05 at 3.18.18 AM.png

But is it worth it, I’ll let answer this question in the following minutes after we understand the structure and practice testing on our beloved Counter App

How could we implement a test for a feature?

Think of it logically, how would you test if something is working or not?

in big words a test is defined by how you will programmatically tell flutter to double check if the output given by a feature is equal to the expected response you planned on receiving an no other

Screen Shot 2023-02-05 at 3.27.58 AM.png

Now let’s switch to the coding part where i’ll show how testing works and after wards how to write tests inside your app.

👉  Let’s do it

Now you need to clone this repo → CounterApp and add bloc_test & test dependency

dependencies:
  bloc_test: ^9.1.0
  test: ^1.22.0
Enter fullscreen mode Exit fullscreen mode

in test folder we’ll have our test files, a little hint i can give you is that every feature of your app need to be tested in the first place inside the test folder should be kind of symmetrical to the features from within tour app, so all you have to crate the folder symmetrically to the lib folder inside the test folder and add _test to the name of the file.

In this case we’ll have a counter_cubit_test.dart in which we’ll test out the functionality of our counter feature.

Screen Shot 2023-02-06 at 5.09.41 AM.png

👉  Remember that all the counter feature does is to increment or decrement a counter value based on which button is pressed by the user.

So inside the test file we need to start by create a MainFunction inside this main function we’ll go right ahed and create a group with the name of CounterCubit

import 'package:test/test.dart';

void main() {
  group('CounterCubit', () {

  });
}
Enter fullscreen mode Exit fullscreen mode

group → is actually a way of organizing multiple test for a feature.

For Example:

inside our CounterCubit group we’ll write all the necessary test for the counter feature.

In a group you can also share a common setup and teardown functions across all the available test

setUp(() {});
tearDown(() {});
Enter fullscreen mode Exit fullscreen mode

What is the purpose of these functions?

  • Inside the setup function you can instantiate

    For Example:

    The objects our tests will be working with, in our case, we’ll instantiate the CounterCubit so that access it later on in our test

    so setup is mainly as its name is implying a function which will be called to create and initialize the necessary data tests will work with.

    on the other hand teardown function

void main() {
  group('CounterCubit', () {
    late CounterCubit counterCubit;

    setUp(() {
      counterCubit = CounterCubit();
    });

    tearDown(() {
      counterCubit.close();
    });
  });
}
Enter fullscreen mode Exit fullscreen mode
  • teardown Function

    💡 Is a function that will get called after each test is run and if `group` it will apply of course only to the test in that `group` inside of this perhaps we can `close` the created `Cubit`

Now the time has finally come for us to write our first test which is going to be checking the InitialState of our CounterCubit to is equal to CounterState with a CounterValue of Zero .

We’re going this by creating a test function and give it a description which should denote the purpose of it, in this case in going to be “initial state of CounterCubit is CounterState(counterValue:0)”

group('CounterCubit', () {
    late CounterCubit counterCubit;

    setUp(() {
      counterCubit = CounterCubit();
    });

    tearDown(() {
      counterCubit.close();
    });
    test('initial state of CounterCubit is CounterState(counterValue:0)', () {

    });
  });
Enter fullscreen mode Exit fullscreen mode

How do we check this?

I told you earlier that the purpose of any test is to check that the output of the feature is actually equal to the expected output nothing else

To do this, all we need is the except function which will take two main important arguments ( the actual value returned by our InitialState and the expected value which we’re expecting to be received )

So our InitialState returned when the Cubit created will be counterCubit.state and the expected value should be a CounterState which has the counterValue: 0

test('initial state of CounterCubit is CounterState(counterValue:0)', () {
     expect(counterCubit.state, CounterState(counterValue: 0));
});
Enter fullscreen mode Exit fullscreen mode

We can run this test by typing dart run test in terminal

//todo
Enter fullscreen mode Exit fullscreen mode

OR rather by clicking the run button next to the group .

So if we run this test we will surprisingly receive a complete failure with this Message

Expected: <Instance of 'CounterState'>
  Actual: <Instance of 'CounterState'>
Enter fullscreen mode Exit fullscreen mode

in which we expected a instance of CounterState and we actually receive an instance of CounterState , here you can actually start to understand why testing is such an amazing tool, it has already signaled us a problem with our app.

👉  Remember that the application worked perfectly when we manually tested it, how come that test fails then

since it tell us that both the expected and actual output are instance of CounterState and we know that both should have a Zero value inside that means that the instances are still different somehow

Screen Shot 2023-02-05 at 4.46.38 AM.png

and that’s due the fact that inside Dart language two instances of the same exact class aren’t equal even though they are basically identical this is happening because these two instances are stored in a different part of the memory and Dart compares their location in memory instead of their values hence making them not equal.

You can override this behavior really simple by using a really popular library you may have already heard about → Equatable

in the backend, Equatable is just a simple tool which overrides the equal operator and the HashCode for every class that extends it hence tricking Dart into comparing the instances by value rather than by where they’re placed in the memory.

So if you want the functionality of comparing to instances like we do in our test, Equatable is the way to go.

equatable | Dart Package

Ok, so all we need to do inside our app right now is to add The Equatable dependency in pubspec.yaml file

dependencies:
  equatable: ^2.0.5
Enter fullscreen mode Exit fullscreen mode

And then extend the CounterState class with it

import 'package:equatable/equatable.dart';

class CounterState extends Equatable {
  final int counterValue;
  final bool? wasIncremented;
  const CounterState({
    required this.counterValue,
    this.wasIncremented,
  });
}
Enter fullscreen mode Exit fullscreen mode

NOTE:

that now we’ll get a warning telling us that need to override the props of the Equatable class

👉  props → are just a way of indicating Equatable which are the fields in our class that we want to be compared while applying the equal operator

so we’re gonna pass both the counterValue and wasIncremented attributes inside the props list

import 'package:equatable/equatable.dart';

class CounterState extends Equatable {
  final int counterValue;
  final bool? wasIncremented;
  const CounterState({
    required this.counterValue,
    this.wasIncremented,
  });

  @override
  List<Object?> get props => [counterValue, wasIncremented]; // <-
}
Enter fullscreen mode Exit fullscreen mode

Now whenever we will compare two CounterStates , Dart will compare them attributes by attribute in order to see whether they’re equal or not.

so right now if you go back to our test file and run the test, the test should finally pass since our expected and actual CounterState had the same counterValue which is Zero .

Screen Shot 2023-02-06 at 7.33.22 AM.png

Now it is time for us to test out the functionality of the Increment and Decrement functions from inside our counter feature because these are the most important right

⇒ for this we’ll use the blocTest function from inside the bloc_test dependency.

We’ll use this because we need to test the output as a response to the increment or decrement functions.

  • We’ll start with the test for the increment functionality

    So firstly we need to describe it properly

    “the CounterCubit should emit a CounterState(counterValue:1, wasIncremented:true) when the increment function is called”

    👉  To do this, we’ll use the trio parameters of build , act, expect from inside blocTest function.

    blocTest(
          'the CounterCubit should emit a CounterState(counterValue:1,'
                'wasIncremented:true) when the increment function is called',
       build: null,
       act: null,
       expect: null,
    );
    
    • build

      💡 Is a function that will return `current instance` of the `CounterCubit` in order to make it available to the testing process

      build: ()⇒counterCubit,

    • act

      💡 Is a function that will take the `cubit` and will return the `action` applied on it.

      in our case is the increment function → act: (cubit)⇒ cubit.increment()

    • expect

      💡 Is a function that will return an `iterable list` which will verify if the order of the state and the actual emitted state correspond with the emitted ones and no other.

      the counterCubit emits only a single state in this example we’ll place it inside the list accordingly

      expect: ()⇒ [const CounterState(counterValue: 1, wasIncremented: true)],

    blocTest(
        'the CounterCubit should emit a '
        'CounterState(counterValue:1, wasIncremented:true)'
        ' when the increment function is called',
      build: () => counterCubit,
      act: (cubit) => cubit.increment(),
      expect: () => [const CounterState(counterValue: 1, wasIncremented: true)],
    );
    
  • test decrement function

    The same procedure applies also to decrement function, the only difference being that will act by calling the decrement function and that will expect a CounterState with a counterValue of -1 inside of 1 since we subtract 1 from the initial counterValue .

    blocTest(
        'the CounterCubit should emit a '
        'CounterState(counterValue:-1, wasIncremented:false)'
        ' when the decrement function is called',
      build: () => counterCubit,
      act: (cubit) => cubit.decrement(),
      expect: () =>
            [const CounterState(counterValue: -1, wasIncremented: false)],
    );
    

Let’s run all the test now

Screen Shot 2023-02-06 at 8.21.19 AM.png

Every Thing Passed 🎉  🎉

⇒ Now we can know for sure that the CounterCubit works as it should.

🤔 Think about this scenario for example:

Let’s say you were moved away from this project and another guy comes into place to continue the development, he doesn’t know Dart that well and he think that the Equatable package is not needed and that application can work just fine without it so he refactors the code, runs the app and indeed the app is still working perfectly but underneath if we run the CounterCubit test we can see that if there would be apart inside our app comparing two identical states, the output would have been completely different.

this is exactly why app testing shouldn’t be skipped, it may seem like the refactoring works fine but in reality it isn’t.

so i guess now you understood why it’s worth spending the extra time writing tests for user app.


Top comments (0)