flutter
/

Flutter Cubit: Simplified State Management with BLoC Pattern

Last Sync: Today

On this page

12
0%
Beginner
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterBeginner

Flutter Cubit: Simplified State Management with BLoC Pattern

If you're looking for a simple yet powerful state management solution in Flutter, Cubit is an excellent choice. It's a lightweight alternative to full Bloc, using methods instead of events to emit states. This guide will teach you everything you need to know to start using Cubit in your Flutter apps.

What is Cubit?

Cubit is a simplified version of the BLoC pattern. It extends BlocBase and uses methods to emit states. You define a state class, a Cubit class, and then call methods that emit new states. There are no events to define, making it perfect for simple to moderate complexity flows.

Setting Up flutter_bloc

Add the dependency to your pubspec.yaml:

YAMLRead-only
1
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.5
  equatable: ^2.0.5  # Recommended

Run flutter pub get to install.

Creating a Cubit

Let's build a simple counter Cubit. First, define the state class (immutable) and then the Cubit.

DARTRead-only
1
import 'package:equatable/equatable.dart';

class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);

  @override
  List<Object?> get props => [value];
}
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_state.dart';

class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

Using Cubit in the UI

Use BlocProvider to give the Cubit to the widget tree. It also automatically disposes the Cubit when the widget is removed.

DARTRead-only
1
BlocProvider(
  create: (context) => CounterCubit(),
  child: MyHomePage(),
);

Use BlocBuilder to rebuild when the state changes. It listens to the Cubit and calls the builder with the latest state.

DARTRead-only
1
BlocBuilder<CounterCubit, CounterState>(
  builder: (context, state) {
    return Text('Count: ${state.value}', style: TextStyle(fontSize: 24));
  },
);

Access the Cubit via context.read<CounterCubit>() to call its methods. This does not cause a rebuild.

DARTRead-only
1
ElevatedButton(
  onPressed: () => context.read<CounterCubit>().increment(),
  child: Text('Increment'),
);

Complete Counter Example

DARTRead-only
1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

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

// State
class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);

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

// Cubit
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

// App
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Cubit Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, CounterState>(
          builder: (context, state) {
            return Text('Count: ${state.value}', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Handling Async Operations

Cubit can also handle asynchronous work. Use try-catch and emit loading, success, and error states.

DARTRead-only
1
abstract class DataState extends Equatable {}
class DataInitial extends DataState {}
class DataLoading extends DataState {}
class DataLoaded extends DataState {
  final String data;
  const DataLoaded(this.data);
  @override List<Object?> get props => [data];
}
class DataError extends DataState {
  final String message;
  const DataError(this.message);
  @override List<Object?> get props => [message];
}

class DataCubit extends Cubit<DataState> {
  DataCubit() : super(DataInitial());

  Future<void> fetchData() async {
    emit(DataLoading());
    try {
      final result = await api.getData();
      emit(DataLoaded(result));
    } catch (e) {
      emit(DataError(e.toString()));
    }
  }
}

Side Effects with BlocListener

Use BlocListener to perform side effects (navigation, snackbars) without rebuilding the UI.

DARTRead-only
1
BlocListener<DataCubit, DataState>(
  listener: (context, state) {
    if (state is DataError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
  child: ...
);

Cubit vs Bloc: When to Use Cubit

  • Simple UI state – counters, toggles, forms, etc.
  • No need for event transformation – debouncing, throttling, or complex stream operations.
  • Prefer minimal boilerplate – just methods, no event classes.
  • Small to medium apps where complexity is low.

Best Practices

  • Use Equatable for states – Prevents unnecessary rebuilds.
  • Keep states immutable – Always create new state instances.
  • Place BlocProvider at the appropriate level – Higher for shared cubits, lower for scoped ones.
  • Use BlocBuilder only where needed – Granular rebuilds improve performance.
  • Handle loading/error states – Always provide feedback to the user.
  • Keep business logic in the cubit – UI should only call methods and react to states.

Common Mistakes

  • ❌ Mutating state properties – Leads to bugs and no UI updates. ✅ Always create a new state with emit.
  • ❌ Calling emit after cubit is closed – Throws an error. ✅ Check isClosed before emitting if needed.
  • ❌ Creating BlocProvider inside build – Recreates the cubit on every rebuild, losing state. ✅ Use BlocProvider outside build.
  • ❌ Not using Equatable – Causes unnecessary rebuilds because states are never considered equal. ✅ Extend Equatable and list props.
  • ❌ Using BlocBuilder for side effects – Can cause infinite loops. ✅ Use BlocListener.

Testing Cubit

Cubits are easy to test. Just create an instance, call methods, and assert states.

DARTRead-only
1
test('increment increases count', () {
  final cubit = CounterCubit();
  cubit.increment();
  expect(cubit.state, CounterState(1));
});

Conclusion

Cubit offers a simple, intuitive way to manage state in Flutter apps. With its method-based approach and integration with flutter_bloc, you can build reactive UIs with minimal boilerplate. Start with Cubit, and when your app grows, you can always introduce Bloc for advanced flows.

Try it yourself

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

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

class CounterState extends Equatable {
  final int value;
  const CounterState(this.value);

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

class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Cubit Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, CounterState>(
          builder: (context, state) {
            return Text('Count: ${state.value}', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: Icon(Icons.add),
          ),
          const SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

How do you emit a new state in a Cubit?

A
By calling `state = newState`
B
By calling `emit(newState)`
C
By returning the new state
D
By calling `setState`
Q2
of 3

Which widget is used to provide a Cubit to the widget tree?

A
BlocBuilder
B
BlocProvider
C
BlocListener
D
CubitProvider
Q3
of 3

Why is Equatable recommended for Cubit states?

A
It makes states serializable
B
It prevents unnecessary UI rebuilds by enabling value-based equality
C
It allows copyWith methods
D
It automatically emits states

Frequently Asked Questions

Is Cubit a replacement for Bloc?

No, Cubit is a simplified version. Bloc offers more features like event transformation, debouncing, and advanced stream handling. Choose Cubit for simpler cases, Bloc for complex ones.

Do I need to use Equatable with Cubit?

It's highly recommended. Without it, BlocBuilder may rebuild even when the state hasn't logically changed, because the default == compares references.

Can I use Cubit with multiple screens?

Yes, provide the Cubit at a higher level in the widget tree (e.g., using BlocProvider in a parent widget) so that all child screens can access it.

How do I handle async operations in Cubit?

Define a method that is async, use try-catch, and emit loading, success, and error states at appropriate times.

How do I test a Cubit that uses async methods?

Use expectLater or await the method call and then check the final state. You can also use blocTest from the bloc_test package, which works for both Cubit and Bloc.

Previous

bloc bloc class

Next

bloc emit

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.