Api BLoC

Pub Version codecov

Flutter widgets designed to simplify the implementation of the BLoC pattern for REST APIs within an MVC architecture. Significantly reduces boilerplate code by automating BLoC pattern and test generation for handling REST API interactions.

Features

  • Significantly reducing boilerplate code of bloc pattern to interact with REST API.
  • Generic classes for handling various API states such as loading, success, & error for READ states and idle, loading, success, failed & error for WRITE states.
  • Customizable builder and listener functions to respond to state changes.
  • Generate api bloc pattern & bloc test on command.

Getting Started

To use this library, add api_bloc as a dependency in your pubspec.yaml file.

dependencies:
  api_bloc: ^3.0.1

and to use api_bloc_cli run this command in terminal.

dart pub global activate api_bloc

Fetching Scenario

import 'package:api_bloc/api_bloc.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)));
  }
}

Put the controller inside the ApiBloc widget.

import 'package:api_bloc/api_bloc.dart';

ApiBloc(
  controller: UserDetailController(),
  child: BlocBuilder<UserDetailController, ReadStates>(
    builder: (context, state, child) {
      if (state is ReadSuccessState<UserDetailModel>) {
      } else if (state is ReadErrorState) {}
      return Text(state.message);
    },
  ),
);

When the controller is first initiated, on ReadController it automatically runs the request function. If you want to rerun it, you can do it by calling:

controller.run();

Submitting Scenario

import 'package:api_bloc/api_bloc.dart';

class UserUpdateController extends WriteController {
  @override
  Future<void> onRequest(Map<String, dynamic> args) async {
    // Delay to make the loading state more noticable.
    await Future.delayed(const Duration(milliseconds: 300));

    // Emit your success and failed state here ↓↓
    if (isSuccess) {
    emit(const WriteSuccessState<UserUpdateSuccessModel>(
        data: UserUpdateSuccessModel.test()));
    } else {
    emit(const WriteFailedState<UserUpdateFailedModel>(
        data: UserUpdateFailedModel.test()));
    }
  }
}

Put the controller inside the ApiBloc widget.

import 'package:api_bloc/api_bloc.dart';

ApiBloc(
  controller: UserUpdateController(),
  child: BlocConsumer<UserUpdateController, WriteStates>(
    listener: (context, state) {
      if (state is WriteSuccessState<UserUpdateSuccessModel>) {
      } else if (state is WriteFailedState<UserUpdateFailedModel>) {
      } else if (state is WriteErrorState) {}
    },
    builder: (context, state, child) {
      if (state is WriteLoadingState) {}
      return Text(state.message);
    },
  ),
);

Unlike the ReadController, the initial state of WriteControllerRequest is idle state, so to run the request you need to trigger the controller.run() manually.

Generating Api Bloc Structure (Optional)

To quickly create a module, for example GET detail and GET list, also PUT update, POST create, and DELETE for a module called USER using this library, run this command in terminal:

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

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 

Sentry Integration

You also can integrate this library with Sentry by creating custom controllers like this:

abstract class ReadSentryController extends BlocRequest<ReadStates> {
  ReadSentryController({
    this.autoRun = true,
    JSON args = const {},
  }) : super(value: const ReadLoadingState()) {
    if (autoRun) run(args);
  }

  @override
  Future<void> run([JSON args = const {}]) async {
    emit(const ReadLoadingState());
    final http = SentryHttpClient();
    try {
      await onRequest(http, args);
    } catch (e, s) {
      await onError(e, s);
    } finally {
      http.close();
    }
  }

  Future<void> onRequest(SentryHttpClient http, JSON args);

  Future<void> onError(dynamic e, StackTrace s) async {
    await Sentry.captureException(e, stackTrace: s);
    emit(ReadErrorState(message: e.toString(), data: s));
  }

  final bool autoRun;
}

abstract class WriteControllerSentryController extends BlocRequest<WriteControllerStates> {
  WriteControllerSentryController() : super(value: const WriteControllerIdleState());

  @override
  Future<void> run([JSON args = const {}]) async {
    emit(const WriteControllerLoadingState());
    final http = SentryHttpClient();
    try {
      await onRequest(http, args);
    } catch (e, s) {
      await onError(e, s);
    } finally {
      http.close();
    }
  }

  Future<void> onRequest(SentryHttpClient http, JSON args);

  Future<void> onError(dynamic e, StrackTrace s) async {
    await Sentry.captureException(e, stackTrace: s);
    emit(WriteControllerErrorState(message: e.toString(), data: s));
  }
}

Whenever you want to interact with the API, create a controller like this:

class ReadUserRequest extends ReadSentryController {

  Future<void> onRequest(http, args) async {
    await Future.delayed(const Duration(milliseconds: 300));
    final response = await http.get(Uri('http://baseUrl/api/user/123'));
    emit(ReadSuccessState<UserModel>(data: UserModel.fromJSON(jsonDecode(response.body))));
  }
}

Example

Libraries

api_bloc
A Flutter library for managing API calls using the BLoC pattern. This library provides a set of classes and utilities to simplify API calls and manage state changes.