Using [warped_bloc] to reduce boilerplate code when using BLoC pattern Flutter.
This Article assumes that you’re already familiar with implementing BLoC pattern in Flutter.
First thing’s First. You can install warped_bloc
as follows :
Table of Contents
- BlocWrapper
- AsyncCubit
- defaultListener() and defaultBuilder()
- Configuring defaultListener() and defaultBuilder()
- Handling errors in AsyncCubit.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
This package by default comes with some very handy features :
BlocWrapper
You can wrap your widgets with BlocWrapper
to finally end the uneasyness of having to maintain use of BlocBuilder
, BlocListener
and BlocConsumer
.
BlocBuilder
Functionality can be achieved as :
BlocWrapper(
builder: (context, state) => SomeWidget(),
)
BlocListener
Functionality can be achieved as :
BlocWrapper(
listener: (context, state) {...},
child: SomeWidget(),
)
BlocConsumer
Functionality can be achieved as :
BlocWrapper(
listener: (context, state) {...},
builder: (context, state) => SomeWidget(),
)
AsyncCubit
When you want to write a Bloc
that deals with asynchronous logic you’ll always find yourself creating 4 redundant states :
- Loading State
- Loaded State or Success State
- Error State
- Initial State
With warped_bloc
it’s automatically handled for you so that you only need to create a “Loaded State” without having to worry about other states.
For this, you need your blocs to extends AsyncCubit
and then create a Loaded or Success State (This state is known as data state in warped_bloc
world). So, it’s done as follows :
For GET request :
class UsersLoaded extends DataState<List<User>> {
const UsersLoaded({required List<User> data}) : super(data: data);
}
class UsersBloc extends AsyncCubit {
...
fetch() async {
emit(LoadingState());
try{
var data = await fetchLogic();
emit(UsersLoaded(data: data));
} catch(e) {
emit(ErrorState(message: e.toString()));
}
}
...
}
You’ll realize that emitting LoadingState
and ErrorState
is something that we do very frequently so, there is a handler for that in warped_bloc
so that you can write as minimal code as possible :
// Reduced Version
class UsersLoaded extends DataState<List<User>> {
const UsersLoaded({required List<User> data}) : super(data: data);
}
class UsersBloc extends AsyncCubit {
...
fetch() {
handleDefaultStates(() async{
var data = await fetchLogic();
emit(UsersLoaded(data: data));
});
}
...
}
For POST request :
class UserCreated extends DataState<String> {
const UserCreated({required String message}) : super(data: message);
}
class UsersActionBloc extends AsyncCubit {
...
createUser(User user) {
handleDefaultStates(() async{
await createUserLogic(user);
emit(UsersLoaded(message: "User Created Successfully"));
});
}
...
}
But, then again to handle the states you’ll have to go and use a if else statement inside a BlocWrapper
which is very boring. So, there is a simplification for that.
defaultBuilder()
Let’s say we want to handle fetching users in our GUI. The code for that will look like :
...
Widget build(BuildContext context) {
return Scaffold(
body: ...(
...
child: BlocWrapper<UsersBloc, BlocState>(
builder: defaultBuilder<BlocState, List<Data>, ErrorDataType>(
onLoading: () => LoadingWidget(),
onData: (List<Data> data) => ListingWidget(data),
onError: (ErrorState state) => ErrorWidget(state.message),
),
),
...
),
);
}
...
In fact, you don’t really need that onLoading
and onError
callback because the package takes care of that automatically. So, the minimal code will look like :
...
BlocWrapper<UsersBloc, BlocState>(
builder: defaultBuilder<BlocState, List<Data>, ErrorDataType>(
onData: (List<Data> data) => ListingWidget(data),
),
),
...
defaultListener()
Let’s say we want to listen to a our UserActionBloc
we created before. The code for that will look like :
...
BlocWrapper<UsersActionBloc, BlocState>(
listener: defaultListener<BlocState, String, String>(
onLoading: (context) {...},
onData: (context, String data) {...},
onError: (context, state) {...},
),
child: ...,
)
...
warped_bloc
is smart enough to know how to show loading
and error
so, the minimal code will look like :
...
BlocWrapper<UsersActionBloc, BlocState>(
listener: defaultListener(),
child: ...,
)
...
Customizing Loading and Errors in "defaultBuilder”
and "defaultListener"
While defaultBuilder
can show default Loading and Error Widgets and defaultListener
are able to show default Loading Modal and Error dialog, it is possible that you’d want different behaviour for your apps. So, you can customize defaultBuilder
and defaultListner
as follows :
void main() {
...
WidgetsFlutterBindings.ensureInitialized();
DefaultBuilderConfig.configure(
onLoading: () {...},
onError: <dynamic>(state) {...},
);
DefaultListenerConfig.configure(
onLoading: (context) {...},
onData: (context, data) {...},
onError: (context, errState) {...},
onStateChange: (context) {...},
);
runApp(MyApp());
...
}
You can remove the defaultListener config as :
void main() {
...
WidgetsFlutterBindings.ensureInitialized();
DefaultListenerConfig.configureToDoNothing();
runApp(MyApp());
...
}
Handing Errors inside “AsyncCubit”
Since we’ve will be using handleDefaultStates()
inside AsyncCubit
to handle Loading and Error States it might be useful to know how Errors are handled inside handleDefaultStates()
.
...
fetch() {
...
handleDefaultStates(() async{
/// --- Main Code Block
});
...
}
...
The assumption made is that any code that runs inside Main Code Block
must be of type Failure
. If there is some unexpected error inside Main Code Block
then, the error message displayed by defaultBuilder
and defaultListener
will include the stack trace of what went wrong.
Finally, you can check out the package yourself on pub.dev
Also, you can check the github repo :