Using [warped_bloc] to reduce boilerplate code when using BLoC pattern Flutter.

Sushan Shakya
4 min readDec 1, 2022

--

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

  1. BlocWrapper
  2. AsyncCubit
  3. defaultListener() and defaultBuilder()
  4. Configuring defaultListener() and defaultBuilder()
  5. 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 :

  1. Loading State
  2. Loaded State or Success State
  3. Error State
  4. 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 :

--

--