Simplify API Requests in Flutter with BLoC Pattern: Reducing Boilerplate Code

Nialixus
5 min readJul 23, 2023

--

Repetitive work
Repetitive Work by DALL-E

Introduction

Various states on fetching and submitting data (GET replaced by Read and SEND replaced by Write)

In recent years, as I worked on various Flutter projects involving API integrations, I noticed a recurring pattern when it came to fetching and submitting data using the BLoC pattern. For fetching data, there were three absolute (READ) states: loading, success, and error.

On the other hand, when submitting data, there were five (WRITE) states: idle, loading, success, failed, and error. Recognizing the repetitiveness of this process, I decided to create a library that would streamline API requests using the BLoC pattern, significantly reducing boilerplate code and improving efficiency.

Get Started

https://pub.dev/packages/api_bloc

Before we start using this package, it’s important to note that there are two types of interactions with a REST API: reading from the server using the GET method, and writing data to the server using methods like POST, PUT, PATCH, and DELETE.

Typically, when using the BLoC pattern, we need to build the bloc/cubit, states, models, and views (widgets and pages). However, with the API Bloc library, we only need to build the model, views, and controller. As an example, we’re going to use the Reqres API.

1. Install the library

dependencies:
api_bloc: ^3.0.1
# You can use http, graphql or others package that can interact with API
dio: ^5.3.0

2. Create the controller

To fetch data, we typically need three states: loading, success, and error. By extending the ReadController, the only task left is to emit the success state. Let's create the controller in controllers/get_user_controller.dart.

class GetUserController extends ReadController {
@override
Future<void> onRequest(Map<String, dynamic> args) async {
// Mock Delay
await Future.delayed(const Duration(seconds: 1));

Response response = await Dio().get(
'https://reqres.in/api/users/2',
onReceiveProgress: (received, total) {
emit(ReadLoadingState<double>(data: received / total));
},
);

emit(ReadSuccessState<GetUserModel>(
data: GetUserModel.fromJSON(response.data)));
}
}

When it comes to submitting data, we typically encounter five states: idle, loading, success, failed, and error. However, by extending the WriteController, we only need to emit the success and failed states. Let's create the controller in controllers/create_user_controller.dart.


class CreateUserController extends WriteController {
final TextEditingController name = TextEditingController();
final TextEditingController job = TextEditingController();

@override
Future<void> onRequest(Map<String, dynamic> args) async {
if (name.text.isEmpty) {
emit(WriteFailedState(message: 'Name cannot be empty', data: name));
} else if (job.text.isEmpty) {
emit(WriteFailedState(message: 'Job cannot be empty', data: job));
} else {
await Future.delayed(const Duration(seconds: 1));

Response response = await Dio().post(
'https://reqres.in/api/users/2',
data: FormData.fromMap({
'name': name.text,
'job': job.text,
}),
onReceiveProgress: (received, total) {
emit(WriteLoadingState<double>(data: received / total));
},
);

if (response.statusCode == 201) {
emit(WriteSuccessState<CreateUserModel>(
data: CreateUserModel.fromJSON(response.data)));
} else {
emit(WriteFailedState(
data: response.data,
message: "Expected response code output is 201"));
}
}
}

@override
void dispose() {
name.dispose();
job.dispose();
super.dispose();
}
}

3. Implementing the Controller in Widget

Now, in the fetch scenario. We’ll start by creating a page in views/get_user_view.dart. This page will serve as the interface for interacting with the fetched data.

import 'package:api_bloc/api_bloc.dart';

class GetUserView extends StatelessWidget {
const GetUserView({super.key});

@override
Widget build(BuildContext context) {
return ApiBloc(
controller: GetUserController(),
builder: (context, controller) => Scaffold(
appBar: AppBar(title: const Text('GET Request')),
body: RefreshIndicator(
onRefresh: controller.run,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
alignment: Alignment.center,
height: MediaQuery.sizeOf(context).height - kToolbarHeight,
child: BlocBuilder<GetUserController, ReadStates>(
builder: (context, state, child) {
switch (state) {
case ReadSuccessState<GetUserModel> _:
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(state.data.avatar),
Text(state.data.fullName)
],
);
case ReadErrorState<StackTrace> _:
return Text('Oops something is wrong\n${state.message}');
default:
return const CircularProgressIndicator();
}
},
),
),
),
),
),
);
}
}

For the submission scenario, let’s create a page in views/create_user_view.dart. This page will serve as the interface for users to input and submit data.

import 'package:api_bloc/api_bloc.dart';

class CreateUserView extends StatelessWidget {
const CreateUserView({super.key});

@override
Widget build(BuildContext context) {
return ApiBloc(
controller: CreateUserController(),
builder: (context, controller) => Scaffold(
appBar: AppBar(title: const Text("POST Request")),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (int x = 0; x < 2; x++)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: TextField(
controller: [controller.name, controller.job][x],
decoration: InputDecoration(
labelText: ["Name", "Job"][x],
border: const OutlineInputBorder(),
),
),
),
BlocConsumer(
controller: controller,
listener: (context, state) {
switch (state) {
case WriteSuccessState<CreateUserModel> _:
return context.alert(
"Successfully creating new user with id #${state.data.id}",
);
case WriteFailedState _:
return context.alert(
"Failed because ${state.message}",
color: Colors.orange,
);
case WriteErrorState _:
return context.alert(
state.message,
color: Colors.red,
);
default:
return;
}
},
builder: (context, state, child) {
switch (state) {
case WriteLoadingState _:
return TextButton(
onPressed: () {},
child: Container(
padding: const EdgeInsets.all(10.0),
color: Colors.blue,
child: const Text(
"Loading ...",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
),
);
default:
return InkWell(
onTap: controller.run,
child: Container(
color: Colors.blue,
padding: const EdgeInsets.all(10.0),
child: const Text(
"SUBMIT",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
),
);
}
},
),
],
),
),
),
);
}
}

Code Generation

Based on the example above, when we create those controllers and related components, we still have to manually write the fixed template pattern. However, by using the API Bloc CLI, you can generate these api bloc and its test templates with a single command, enhancing your productivity.

First thing first, install the CLI.

dart pub global activate api_bloc

Now in your project directory terminal, run this command

dart run api_bloc --create user --read detail,list --write create,update,delete --output lib/src/

It will generate this structure in your project:

📂 lib/src/user/
📄 lib/src/user/user.dart
📄 lib/src/user/controllers/user_detail.dart
📄 lib/src/user/controllers/user_list.dart
📄 lib/src/user/controllers/user_update.dart
📄 lib/src/user/controllers/user_create.dart
📄 lib/src/user/controllers/user_delete.dart
📄 lib/src/user/models/user_detail.dart
📄 lib/src/user/models/user_list.dart
📄 lib/src/user/models/user_update.dart
📄 lib/src/user/models/user_create.dart
📄 lib/src/user/models/user_delete.dart
📄 lib/src/user/views/user_detail.dart
📄 lib/src/user/views/user_list.dart
📄 lib/src/user/views/user_update.dart
📄 lib/src/user/views/user_create.dart
📄 lib/src/user/views/user_delete.dart
📂 test/src/user/
📄 test/src/user/controllers/user_detail.dart
📄 test/src/user/controllers/user_list.dart
📄 test/src/user/controllers/user_update.dart
📄 test/src/user/controllers/user_create.dart
📄 test/src/user/controllers/user_delete.dart
📄 test/src/user/models/user_detail.dart
📄 test/src/user/models/user_list.dart
📄 test/src/user/models/user_update.dart
📄 test/src/user/models/user_create.dart
📄 test/src/user/models/user_delete.dart
📄 test/src/user/views/user_detail.dart
📄 test/src/user/views/user_list.dart
📄 test/src/user/views/user_update.dart
📄 test/src/user/views/user_create.dart
📄 test/src/user/views/user_delete.dart

Conclusion

As a developer working on Flutter projects, dealing with API interactions can become repetitive and time-consuming. The API Bloc library offers a structured approach to handling common API request states, aiming to streamline this process. By reducing redundancy and improving code maintainability, the library enhances developer productivity. With API Bloc, Flutter developers can shift their focus towards creating exceptional user experiences, rather than managing repetitive API request states.

[Example]: https://github.com/Nialixus/api_bloc/tree/main/example
[Package]: https://pub.dev/packages/api_bloc

--

--