Using Cubit for Managing States in Flutter

State management in Flutter simplified

Using Cubit for Managing States in Flutter

There are a plethora of state management solutions in Flutter. Provider, Bloc, RxDart, Riverpod, GetX, the list goes on! However, my preference is Cubit or, as I like to call it, mini Bloc.

So, what is a Cubit?

A Cubit is a class that exposes functions that can be invoked to call state changes.

What does this mean?

If you're coming from C++ and its Qt framework, the way Cubits work is highly similar to Qt's signal and slot mechanism. If you're coming from a different background, don't worry! It's straightforward. Any time you wish to change state, you simply call an emit() function.

A simple application

To showcase the Cubit state management solution, we're going to recreate Flutter's counter app. Create a new Flutter project and add the following packages to your pubspec.yaml file.

flutter_bloc: ^7.0.1
bloc_test: ^8.0.2
equatable: ^2.0.3

Our sample app would look like this.

screenshot

So let's get started! The first thing we should do first is to create the cubit and state. In the lib folder, create a sub-folder called cubits; this is where we will store our cubits and states. From there, make a file called operator_state.dart; this will store the current state of our application. Now add the following line to your operator_state.dart file.

part of 'operator_cubit.dart';

class OperatorState extends Equatable {
  double number;

  OperatorState({required this.number});

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

OperatorState extends Equatable in order to avoid the issue of states not changing in Flutter whenever you use Cubits or Blocs. The problem is avoided by adding the following line of code to the OperatorState class.

@override
List<Object> get props => [this.number];

The number variable, on the other hand, is what we will keep track of. At the top of the file, there is a part of 'operator_cubit.dart', which is the file that will handle state changes; let's create that now!

Let's instantiate our operator_cubit.dart file.

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

part 'operator_state.dart';

class OperatorCubit extends Cubit<OperatorState> {
  OperatorCubit() : super(OperatorState(number: 0.0));
}

Knowing that OperatorCubit will handle the state changes of OperatorState, it is only natural that it extends it as well.

Operations

Based on the picture earlier, our sample app will make use of the 4 basic operations, addition, subtraction, multiplication, and division.

To handle these operations, we will simply just create methods that simulates these operations in the OperatorCubit class.

void add() {
    emit(
      OperatorState(number: state.number + 1),
    );
 }

void substract() {
    emit(
        OperatorState(number: state.number - 1),
    );
 }

 void multiply() {
    emit(
        OperatorState(number: state.number * 2 ),
    );
 }

 void divide() {
    emit(
        OperatorState(number: state.number / 2),
    );
 }

Here we finally get to see the emit() function I was talking about earlier. emit() simply calls a state change in the OperatorState class. For the add() and subtract() methods, we simply add or subtract 1 to the current state number. The multiply() and divide() methods, on the other hand, will multiply or divide the current state number by 2.

The operator_cubit.dart file now looks like this.

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

part 'operator_state.dart';

class OperatorCubit extends Cubit<OperatorState> {
  OperatorCubit() : super(OperatorState(number: 1.0));

  void add() {
    emit(
      OperatorState(number: state.number + 1),
    );
  }

  void substract() {
    emit(
        OperatorState(number: state.number - 1),
    );
  }

  void multiply() {
    emit(
        OperatorState(number: state.number * 2 ),
    );
  }

  void divide() {
    emit(
        OperatorState(number: state.number / 2),
    );
  }
}

Writing Tests

Now that we have the basics of cubits down, we can start discussing how do we test these cubits. Testing Cubits is a simple process. In the test folder, create a sub-folder called cubit_test, there create a file called operator_cubit_test.dart. In the operator_cubit_test.dart file, we import the following.

import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:cubit_testing_basics/cubits/operator_cubit.dart';

We then create a main() function which will contain all of our tests for the OperatorCubit class. Since our test file will contain tests only for the OperatorCubit class, it's best to group the tests together.

void main() {
  group(OperatorCubit, () {

  });
}

From there we can now start setting up our tests.

late OperatorCubit operatorCubit;

//initialize the testing setup
setUp(() {
    operatorCubit = OperatorCubit();
});

//dictates what will happen after the test finishes
tearDown(() {
    operatorCubit.close();
});

We assign the late keyword to the operatorCubit variable because we only instantiate it during the setup of our application. This is simulated by the following code.

setUp(() {
    operatorCubit = OperatorCubit();
});

tearDown() on the other hand disposes off the cubit after the tests finishes.

We then test the initial state of our operatorCubit using the test() function. We first add in a description for our test.

test("Initial state of OperatorCubit is OperatorState(number: 1.0)", () {
    expect(operatorCubit.state, OperatorState(number: 1.0));
});

expect() takes in two parameters, actual, and matcher. actual corresponds to the actual state being emitted, while matcher is what we're expecting to be emitted. For our case, our actual is the operatorCubit.state, while our matcher is the initial state of our application which is OperatorState(number: 1.0)).

For testing our state changes, we make use of the blocTest() function. A blocTest() function will usually make use of 4 parameters.

description: ' ', //not a named parameter
build: () => nameOfCubit,
act: (cubit) => nameOfCubit.method(),
expect: () => [nameOfCubitState(expectedValue)],

With that, lets test our 4 operations!

//test the cubits
blocTest(
  "The cubit should emit a state of OperatorState(number: 2.0) when add() is called",
  build: () => operatorCubit,
  act: (cubit) => operatorCubit.add(),
  expect: () => [OperatorState(number: 2.0)],
);

blocTest(
  "The cubit should emit a state of OperatorState(number: 0.0) when substract() is called",
  build: () => operatorCubit,
  act: (cubit) => operatorCubit.substract(),
  expect: () => [OperatorState(number: 0.0)],
);

blocTest(
  "The cubit should emit a state of OperatorState(number: 2.0) when multiply() is called",
  build: () => operatorCubit,
  act: (cubit) => operatorCubit.multiply(),
  expect: () => [OperatorState(number: 2.0)],
);

blocTest(
  "The cubit should emit a state of OperatorState(number: 0.5) when divide() is called",
  build: () => operatorCubit,
  act: (cubit) => operatorCubit.divide(),
  expect: () => [OperatorState(number: 0.5)],
);

Our operator_cubit_test.dart file is now complete!

import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:cubit_testing_basics/cubits/operator_cubit.dart';

void main() {
  group(OperatorCubit, () {
    late OperatorCubit operatorCubit;

    //initialize the testing setup
    setUp(() {
      operatorCubit = OperatorCubit();
    });

    //dictates what will happen after the test finishes
    tearDown(() {
      operatorCubit.close();
    });

    //test the initial state, number: 1
    test("Initial state of OperatorCubit is OperatorState(number: 1.0)", () {
      expect(operatorCubit.state, OperatorState(number: 1.0));
    });

    //test the cubits
    blocTest(
      "The cubit should emit a state of OperatorState(number: 2.0) when add() is called",
      build: () => operatorCubit,
      act: (cubit) => operatorCubit.add(),
      expect: () => [OperatorState(number: 2.0)],
    );

    blocTest(
      "The cubit should emit a state of OperatorState(number: 0.0) when substract() is called",
      build: () => operatorCubit,
      act: (cubit) => operatorCubit.substract(),
      expect: () => [OperatorState(number: 0.0)],
    );

    blocTest(
      "The cubit should emit a state of OperatorState(number: 2.0) when multiply() is called",
      build: () => operatorCubit,
      act: (cubit) => operatorCubit.multiply(),
      expect: () => [OperatorState(number: 2.0)],
    );

    blocTest(
      "The cubit should emit a state of OperatorState(number: 0.5) when divide() is called",
      build: () => operatorCubit,
      act: (cubit) => operatorCubit.divide(),
      expect: () => [OperatorState(number: 0.5)],
    );
  });
}

Connecting Cubits to the UI

In our main.dart file, import the following.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:cubit_testing_basics/cubits/operator_cubit.dart';

In our main() function, we have a MyApp class which extends StatelessWidget.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Cubit, and Testing Basics'),
    );
  }
}

MyHomePage extends StatefulWidget.

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'The Number is Currently:',
              style: TextStyle(
                fontSize: 20,
              ),
            ),
            SizedBox(
              height: 30,
            ),
           Text(
              '1.0',
              style: Theme.of(context).textTheme.headline3,
           ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: null,
                  child: Text('+'),
                ),
                FloatingActionButton(
                  onPressed: null
                  child: Text('-'),
                ),
              ],
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: null,
                  child: Text('*'),
                ),
                FloatingActionButton(
                  onPressed: null,
                  child: Text('/'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Lets add in our Cubit!

First we wrap our MaterialApp with the BlocProvider widget. BlocProvider will take in the parameter create, there we will pass an instance of OperatorCubit()

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => OperatorCubit(),
      child: MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: 'Flutter Cubit, and Testing Basics'),
      ),
    );
  }
}

In our _MyHomePageState class, we replace the following lines of code...

Text(   
  '1.0',
  style: Theme.of(context).textTheme.headline3,
),

With...

BlocBuilder<OperatorCubit, OperatorState>(
  builder: (context, state) {
    return Text(
      state.number.toString(),
      style: Theme.of(context).textTheme.headline3,
    );
  },
),

BlocBuilder rebuilds the widget tree with the corresponding state change, in our case, with the changes in the number variable.

The last thing we need to add is the onPressed functionality of our FloatingActionButton widgets. Here, we passed on to the onPressed parameter the methods in our OperatorCubit by accessing it through BlocProvider. For example, in your FloatingActionButton with the + operator...

onPressed: () => BlocProvider.of<OperatorCubit>(context).add()

We now do the same thing to the other functions, but this time, using the other methods. Our _MyHomePageState class will now look like this.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'The Number is Currently:',
              style: TextStyle(
                fontSize: 20,
              ),
            ),
            SizedBox(
              height: 30,
            ),
            BlocBuilder<OperatorCubit, OperatorState>(
              builder: (context, state) {
                return Text(
                  state.number.toString(),
                  style: Theme.of(context).textTheme.headline3,
                );
              },
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).add(),
                  child: Text('+'),
                ),
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).substract(),
                  child: Text('-'),
                ),
              ],
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).multiply(),
                  child: Text('*'),
                ),
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).divide(),
                  child: Text('/'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Our main.dart file is now complete!

import 'package:cubit_testing_basics/cubits/operator_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => OperatorCubit(),
      child: MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: 'Flutter Cubit, and Testing Basics'),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'The Number is Currently:',
              style: TextStyle(
                fontSize: 20,
              ),
            ),
            SizedBox(
              height: 30,
            ),
            BlocBuilder<OperatorCubit, OperatorState>(
              builder: (context, state) {
                return Text(
                  state.number.toString(),
                  style: Theme.of(context).textTheme.headline3,
                );
              },
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).add(),
                  child: Text('+'),
                ),
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).substract(),
                  child: Text('-'),
                ),
              ],
            ),
            SizedBox(
              height: 30,
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).multiply(),
                  child: Text('*'),
                ),
                FloatingActionButton(
                  onPressed: () => BlocProvider.of<OperatorCubit>(context).divide(),
                  child: Text('/'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

The finished code can be found here.

Wrapping Up

You have now learned the basics of the Cubit state management solution, and how to test a Cubit in Flutter! If you want to learn more, simply go to the documentation .

If you've found this useful, do consider following me on Twitter, and on LinkedIn.