“BLoC is powerful — but managing loading, success, error, and empty states across different features can quickly become repetitive. I wanted something cleaner. Here’s the pattern I built and now rely on in all my Flutter projects.”
## 🎯 The Problem
If you’ve worked with Flutter BLoC for more than a week, you’ve probably ended up with a BLoC state that looks something like this:
bool isLoading;
bool hasError;
String errorMessage;
List<Comment> comments;
Or worse, you created multiple state subclasses:
class CommentInitial extends CommentState {}
class CommentLoading extends CommentState {}
class CommentSuccess extends CommentState {
final List<Comment> comments;
}
class CommentError extends CommentState {
final String error;
}
This works — but it gets bloated fast, especially when your app scales and you need this for every feature.
## 🧪 The Solution: RequestStatus
Instead of defining multiple flags or subclasses, I created a generic state wrapper to manage idle, loading, success, error, and empty states in a single, reusable class.
enum RequestState { idle, loading, success, error, empty }
class RequestStatus<T> {
final RequestState state;
final T? data;
final String? error;
const RequestStatus._(this.state, {this.data, this.error});
const RequestStatus.idle() : this._(RequestState.idle);
const RequestStatus.loading() : this._(RequestState.loading);
const RequestStatus.success({T? data}) : this._(RequestState.success, data: data);
const RequestStatus.error(String error) : this._(RequestState.error, error: error);
const RequestStatus.empty() : this._(RequestState.empty);
bool get isLoading => state == RequestState.loading;
bool get isSuccess => state == RequestState.success;
bool get isError => state == RequestState.error;
bool get isIdle => state == RequestState.idle;
bool get isEmpty => state == RequestState.empty;
}
**
🧱 How I Use It in BLoC
**
My BLoC state now looks like this:
class CommentState extends Equatable {
final RequestStatus<List<Comment>> commentsStatus;
const CommentState({required this.commentsStatus});
factory CommentState.initial() => CommentState(
commentsStatus: RequestStatus.idle(),
);
CommentState copyWith({RequestStatus<List<Comment>>? commentsStatus}) {
return CommentState(
commentsStatus: commentsStatus ?? this.commentsStatus,
);
}
@override
List<Object?> get props => [commentsStatus];
}
The event handler becomes super clean:
on<FetchComments>((event, emit) async {
emit(state.copyWith(commentsStatus: RequestStatus.loading()));
try {
final comments = await repository.getComments();
emit(state.copyWith(
commentsStatus: comments.isEmpty
? RequestStatus.empty()
: RequestStatus.success(data: comments),
));
} catch (e) {
emit(state.copyWith(commentsStatus: RequestStatus.error(e.toString())));
}
});
## 🧩 The UI Logic is a Dream
final status = context.watch<CommentBloc>().state.commentsStatus;
if (status.isLoading) {
return CircularProgressIndicator();
} else if (status.isError) {
return Text('Error: ${status.error}');
} else if (status.isEmpty) {
return Text('No comments found.');
} else if (status.isSuccess) {
return CommentList(comments: status.data!);
} else {
return SizedBox.shrink(); // idle
}
No more flags, no more switch statements, and no more CommentLoadingState vs. CommentLoadedState spaghetti.
✅ Why This Works So Well
🔁 Reusable: Works across features (comments, users, posts, messages, etc.)
🧹 Cleaner: Replaces tons of boilerplate code
🎯 Focused: UI reacts to a single object with readable getters
📦 Scalable: Easy to plug into any feature with async operations
Conclusion
This tiny abstraction made a huge difference in how I structure and manage state in my Flutter apps. It’s flexible, readable, and keeps both my BLoCs and UIs lightweight.
💬 What Do You Think?
Have you built something similar? Do you see areas where this pattern could be improved further? I’d love to hear your thoughts and ideas — drop them in the comments or connect with me on:
Top comments (0)