In Bloc, states are the source of truth for your UI. They represent what the user sees at any given moment. Designing clean, immutable state classes is crucial for predictable behavior, testability, and performance. This guide covers everything you need to know about states in Bloc.
What is a State in Bloc?
A state is an object that holds data and reflects the current condition of a feature. For example, a LoginState could be initial, loading, success, or failure. The Bloc emits states in response to events, and the UI rebuilds when a new state is received.
Immutability: Why It Matters
States must be immutable. Once created, their properties cannot change. Instead, you create a new instance when you want to update the state. This approach has several benefits:
Predictability – No unexpected side effects.
Performance – Bloc can efficiently compare old and new states using == to decide whether to rebuild.
Debugging – You can track state changes over time (time travel).
Using Equatable
Equatable overrides == and hashCode for you, so you don’t have to write boilerplate. Extend Equatable in your state classes and list the properties in the props getter.
DARTRead-only
1
import'package:equatable/equatable.dart';
abstract classLoginStateextendsEquatable{constLoginState();
@override
List<Object?>getprops=>[];}classLoginInitialextendsLoginState{}classLoginLoadingextendsLoginState{}classLoginSuccessextendsLoginState{
final String userId;constLoginSuccess(this.userId);
@override
List<Object?>getprops=>[userId];}classLoginFailureextendsLoginState{
final String message;constLoginFailure(this.message);
@override
List<Object?>getprops=>[message];}
Common State Patterns
The most common pattern for asynchronous operations (API calls). Define states for each phase.
DARTRead-only
1
abstract classDataStateextendsEquatable{}classDataInitialextendsDataState{}classDataLoadingextendsDataState{}classDataLoadedextendsDataState{
final List<Item> items;DataLoaded(this.items);
@override
List<Object?>getprops=>[items];}classDataErrorextendsDataState{
final String error;DataError(this.error);
@override
List<Object?>getprops=>[error];}
When data can be empty, use an explicit empty state to show a friendly message.
DARTRead-only
1
classDataEmptyextendsDataState{}// In bloc:emit(DataEmpty());// when API returns empty list
Sometimes you need to combine multiple pieces of state. Instead of one huge state class with many nullable fields, use sealed classes (or union types) to represent mutually exclusive states.
DARTRead-only
1
sealed classUserProfileState{}
final classUserProfileLoadingextendsUserProfileState{}
final classUserProfileLoadedextendsUserProfileState{
final User user;
final List<Post> posts;UserProfileLoaded(this.user,this.posts);}
final classUserProfileErrorextendsUserProfileState{
final String message;UserProfileError(this.message);}
State with Multiple Properties
When a state holds multiple pieces of data, you might be tempted to use a single class with nullable fields. But that can lead to invalid combinations (e.g., both loading and data at the same time). Prefer separate state classes or use sealed classes.
DARTRead-only
1
// ❌ Avoid: nullable fields that can be in invalid statesclassCombinedState{
final bool isLoading;
final List<Item>? items;
final String? error;// Possible invalid states: isLoading = true and items != null}// ✅ Prefer: distinct state classes
abstract classItemsState{}classItemsLoadingextendsItemsState{}classItemsLoadedextendsItemsState{
final List<Item> items;ItemsLoaded(this.items);}classItemsErrorextendsItemsState{
final String error;ItemsError(this.error);}
State Inheritance and Reusability
If several states share common properties, use an abstract base class that extends Equatable and include those properties in props.
DARTRead-only
1
abstract classFormStateextendsEquatable{
final bool isValid;
final String? errorMessage;constFormState(this.isValid,this.errorMessage);
@override
List<Object?>getprops=>[isValid, errorMessage];}classFormInitialextendsFormState{constFormInitial():super(false,null);}classFormValidextendsFormState{constFormValid():super(true,null);}classFormErrorextendsFormState{constFormError(String errorMessage):super(false, errorMessage);}
Performance Considerations
Use Equatable – Prevents unnecessary rebuilds by properly comparing states.
Keep states small – Large state objects can slow down equality checks. Split them if possible.
Avoid storing large lists in multiple states – If a list is only needed in one state, don't include it in others.
Use copyWith for immutable updates – Helpful when states have many properties, but prefer separate state classes when possible.
Implementing copyWith (for states with many fields)
If you have a state with many fields and you need to update only one, you can implement a copyWith method.
DARTRead-only
1
classUserSettingsStateextendsEquatable{
final bool darkMode;
final bool notifications;
final String language;constUserSettingsState({
required this.darkMode,
required this.notifications,
required this.language,});
UserSettingsState copyWith({
bool? darkMode,
bool? notifications,
String? language,}){returnUserSettingsState(darkMode: darkMode ??this.darkMode,notifications: notifications ??this.notifications,language: language ??this.language,);}
@override
List<Object?>getprops=>[darkMode, notifications, language];}// Usage:emit(state.copyWith(darkMode:true));
Best Practices
Use sealed classes or abstract base + implementations – Clearly represent distinct states.
Always use Equatable – Avoid manual == and hashCode.
Make states immutable – Use final fields and create new instances for changes.
Name states clearly – LoginLoading, LoginSuccess, LoginFailure are self‑documenting.
Keep states flat – Avoid deep nesting; use distinct classes for different phases.
Test state equality – Write tests to ensure Equatable works as expected.
Common Mistakes
❌ Mutating state properties – Leads to bugs and no UI updates.
✅ Always create new instances.
❌ Forgetting to add Equatable – Causes unnecessary rebuilds because states are never considered equal.
✅ Extend Equatable and list props.
❌ Using nullable fields to represent multiple states – Creates invalid combinations.
✅ Use separate state classes.
❌ Storing large lists in all states – Wastes memory.
✅ Keep data only in the states that need it.
❌ Not handling empty states – UI shows nothing when data is empty.
✅ Add explicit empty state.
Next Steps
Now that you understand state design, learn how to combine them with events and blocs:
States are the foundation of Bloc’s reactivity. By designing immutable, well‑structured state classes, you ensure your app behaves predictably and remains maintainable. Use Equatable, separate state classes for different scenarios, and follow the best practices to build robust Flutter applications.
To enable predictable state changes and efficient UI rebuilding
C
To make code compile faster
D
To use Equatable
Q2
of 3
What is the purpose of Equatable in state classes?
A
To make states serializable
B
To automatically implement `==` and `hashCode` for value-based equality
C
To generate copyWith methods
D
To emit states asynchronously
Q3
of 3
Which pattern is best for representing loading, success, and error states?
A
A single class with isLoading, data, and error nullable fields
B
Three separate state classes (Loading, Loaded, Error)
C
A map of status codes
D
Using a global variable
Frequently Asked Questions
Do I always need to use Equatable with states?
Not strictly, but it's highly recommended. Without it, Bloc may rebuild the UI even when the state hasn't logically changed, because the default == compares references, not values. Equatable makes equality checks based on the state's data, preventing unnecessary rebuilds.
Should I use sealed classes or an abstract class with multiple implementations?
Sealed classes (introduced in Dart 3.0) are the best choice because they ensure exhaustive checking in switch statements. If you're on an older Dart version, use an abstract class with @immutable and multiple concrete classes.
How do I handle states that combine multiple independent data sets?
You can have a single state class that contains multiple fields, but ensure they are all required and the class is immutable. Use copyWith for updates. For complex scenarios, consider splitting into separate blocs.
What's the difference between `Equatable` and `freezed`?
freezed is a code generator that creates immutable classes, copyWith, toString, and more. It automatically handles equality. Equatable is simpler – it only overrides == and hashCode. For complex states, freezed is powerful; for simple ones, Equatable suffices.
How do I test state equality in Bloc tests?
Use expect with blocTest and compare states. If you use Equatable, you can compare states directly. For example: expect(state, LoginSuccess(userId: '123')).