โ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)