base_flutter_bloc 0.0.5 copy "base_flutter_bloc: ^0.0.5" to clipboard
base_flutter_bloc: ^0.0.5 copied to clipboard

Simplify BLoC usage: unified 4-state machine, auto loading/error UI, flushbar notifications, retry, pagination, debounce/throttle, safeEmit and more — all in one lightweight package.

base_flutter_bloc #

Simplify BLoC usage: unified 4-state machine, auto loading/error UI, flushbar notifications, retry, pagination, debounce/throttle, safeEmit and more — all in one lightweight package.


The problem this package solves #

A typical BLoC screen without this package looks like this:

// ❌ Before — repeated on EVERY screen
BlocConsumer<UserBloc, UserState>(
  listener: (context, state) {
    if (state is UserErrorState) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
  builder: (context, state) {
    if (state is UserLoadingState) {
      return const Center(child: CircularProgressIndicator());
    }
    if (state is UserErrorState) {
      return Center(child: Text(state.message));
    }
    if (state is UserSuccessState) {
      return Text(state.user.name);
    }
    return const SizedBox.shrink();
  },
);

With base_flutter_bloc, the same screen becomes:

// ✅ After — clean, no boilerplate
BaseBlocConsumer<UserBloc, User>(
  builder: (context, user) => Text(user.name),
);

Loading spinner, error widget, and error notification — all handled automatically.


Features #

  • 🧱 BaseState<T> — a unified 4-state machine (Initial, Loading, Success, Error) that works for any data type
  • BaseBlocBuilder — builds UI for all four states with a single required parameter
  • 🔔 BaseBlocConsumer — builds UI and shows notifications (flushbar) on error/success automatically
  • 👂 BaseBlocListener — listens to state changes and shows notifications without rebuilding the widget tree
  • 🛠 BaseBloc — base class with executeWithErrorHandling (try/catch elimination) and built-in retry with delay
  • 🟦 BaseCubit — same utilities as BaseBloc but for Cubit-based state management, including safeEmit()
  • 🔍 BaseBlocObserver — plug-and-play debug logger + single hook for crash-reporting services
  • debounce / throttle — ready-made event transformers for search fields and rapid-tap protection
  • 📄 BasePaginationBloc — full pagination lifecycle (first load, load more, refresh) in one abstract class
  • 🔗 BuildContext extensionsaddEvent, watchSuccessData, watchIsLoading, and more
  • 🎨 BaseFlutterBlocConfig — global configuration to replace the default error widget and notification with your own implementations

Getting started #

Add the dependency to your pubspec.yaml:

dependencies:
  base_flutter_bloc: ^<latest_version>

Then run:

flutter pub get

Usage #

1. Define your BLoC #

Extend BaseBloc and emit BaseState<T> subclasses. Use executeWithErrorHandling to avoid writing try/catch every time:

class UserBloc extends BaseBloc<UserEvent, BaseState<User>> {
  final UserRepository _repository;

  UserBloc(this._repository) : super(InitialState()) {
    on<FetchUserEvent>(_onFetchUser);
  }

  Future<void> _onFetchUser(
    FetchUserEvent event,
    Emitter<BaseState<User>> emit,
  ) async {
    emit(LoadingState());

    await executeWithErrorHandling(
      action: () => _repository.getUser(event.id),
      onSuccess: (user) => emit(SuccessState(user)),
      onError: (message) => emit(ErrorState(message)),
    );
  }
}

No try/catch on every handler — executeWithErrorHandling catches exceptions and routes them to onError automatically.


2. Build your screen #

BaseBlocBuilder — UI only

Use it when you only need to render different widgets per state, without side effects.

BaseBlocBuilder<UserBloc, User>(
  // Required: builds the widget when data is loaded
  builder: (context, user) => UserCard(user: user),

  // Optional overrides (all have sensible defaults):
  loadingBuilder: (context) => const MyCustomSkeletonLoader(),
  errorBuilder: (context, message) => MyErrorBanner(message: message),
  initialBuilder: (context) => const WelcomePlaceholder(),
  onRefresh: () => context.read<UserBloc>().add(FetchUserEvent()),
);
State Default behaviour Override
InitialState SizedBox.shrink() initialBuilder
LoadingState CircularProgressIndicator.adaptive() loadingBuilder
ErrorState DefaultErrorWidget with retry button errorBuilder
SuccessState builder (required)

BaseBlocConsumer — UI + notifications

Use it when you need to both render UI and react to state changes (e.g. show a notification on error).

BaseBlocConsumer<UserBloc, User>(
  builder: (context, user) => UserCard(user: user),

  // By default, shows an error flushbar automatically on ErrorState.
  // Set to false to suppress it:
  showDefaultErrorFlushbar: true,

  // Optional: show a success notification when data loads:
  showDefaultSuccessFlushbar: false,

  // Optional: handle errors manually instead of using the default flushbar:
  onError: (context, message) => showDialog(
    context: context,
    builder: (_) => AlertDialog(content: Text(message)),
  ),

  onRefresh: () => context.read<UserBloc>().add(FetchUserEvent()),
);

BaseBlocListener — notifications only

Use it when the widget tree doesn't need to rebuild but you want to react to state transitions (e.g. navigate after success).

BaseBlocListener<AuthBloc, AuthData>(
  showDefaultErrorFlushbar: true,
  onSuccess: (context, data) => Navigator.pushReplacementNamed(context, '/home'),
  child: const LoginForm(),
);

3. Emit states from your BLoC #

// Show a loading spinner
emit(LoadingState());

// Pass your data to the UI
emit(SuccessState(user));

// Show the error widget + flushbar notification
emit(ErrorState('Failed to load user'));

// Show the error widget WITHOUT a flushbar notification
emit(ErrorState('Not critical error', showFlushbar: false));

// Attach the raw exception for debugging
emit(ErrorState('Something went wrong', error: exception));

4. Customize globally with BaseFlutterBlocConfig #

Wrap your app (or any subtree) with BaseFlutterBlocConfig to replace the default error widget and/or notification across the entire app — without touching individual screens.

void main() {
  runApp(
    BaseFlutterBlocConfig(
      // Replace DefaultErrorWidget with your own design
      errorWidgetBuilder: (context, message, onRetry) => MyErrorWidget(
        message: message,
        onRetry: onRetry,
      ),

      // Replace the flushbar with a SnackBar, toast, etc.
      showFlushBarCallback: ({
        required context,
        message,
        title,
        isError = false,
        messageColor,
        titleColor,
        durationSeconds = 3,
      }) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(message ?? ''),
            backgroundColor: isError ? Colors.red : Colors.green,
          ),
        );
      },

      child: const MyApp(),
    ),
  );
}

If BaseFlutterBlocConfig is not provided, the package uses its built-in defaults — so existing code requires zero changes.


5. Use BaseCubit for simpler state management #

When you don't need traceable events, extend BaseCubit instead of BaseBloc. It has the same executeWithErrorHandling utility plus safeEmit — which guards against emitting after the cubit is closed.

class UserCubit extends BaseCubit<BaseState<User>> {
  final UserRepository _repo;
  UserCubit(this._repo) : super(InitialState());

  Future<void> fetchUser(String id) async {
    safeEmit(LoadingState());          // safe even if user navigates away
    await executeWithErrorHandling(
      action: () => _repo.getUser(id),
      onSuccess: (user) => safeEmit(SuccessState(user)),
      onError: (msg)  => safeEmit(ErrorState(msg)),
    );
  }
}

BaseBlocBuilder, BaseBlocConsumer, and BaseBlocListener all work with BaseCubit exactly the same way as with BaseBloc.


6. Automatic retries in executeWithErrorHandling #

Both BaseBloc and BaseCubit support retrying a failed request.

await executeWithErrorHandling(
  action: () => _repo.getUser(event.id),
  onSuccess: (user) => emit(SuccessState(user)),
  onError:   (msg)  => emit(ErrorState(msg)),
  retries:     3,                          // retry up to 3 times
  retryDelay:  const Duration(seconds: 2), // wait 2 s between attempts
);

If all attempts fail, onError is called once with the last exception message.


7. Set up BaseBlocObserver #

Register once in main() to get structured debug logs for every state transition and a global hook for crash-reporting services.

void main() {
  Bloc.observer = BaseBlocObserver(
    // Forward errors to your crash reporter (e.g. Firebase Crashlytics):
    onErrorCallback: (bloc, error, stackTrace) {
      FirebaseCrashlytics.instance.recordError(error, stackTrace);
    },
    logTransitions: true,  // log every state change (debug mode only)
    logEvents:      true,  // log every incoming event
    logCreations:   false, // log when blocs are instantiated
    logClosings:    false, // log when blocs are disposed
  );
  runApp(
    BaseFlutterBlocConfig(child: const MyApp()),
  );
}

All console output is gated behind kDebugMode — zero noise in release builds.


8. Debounce and throttle event transformers #

Pass a transformer to Bloc.on<E>() to control how quickly events are processed.

debounce — ideal for search fields

Ignores events until the user stops firing them for the given duration. Only the last event in a burst is processed.

class SearchBloc extends BaseBloc<SearchEvent, BaseState<List<Product>>> {
  SearchBloc(this._repo) : super(InitialState()) {
    on<SearchQueryChanged>(
      _onQueryChanged,
      transformer: debounce(const Duration(milliseconds: 400)),
    );
  }

  Future<void> _onQueryChanged(
    SearchQueryChanged event,
    Emitter<BaseState<List<Product>>> emit,
  ) async {
    emit(LoadingState());
    await executeWithErrorHandling(
      action: () => _repo.search(event.query),
      onSuccess: (results) => emit(SuccessState(results)),
      onError:   (msg)     => emit(ErrorState(msg)),
    );
  }
}

throttle — ideal for buttons

Forwards the first event immediately, then ignores subsequent events for the given duration. Prevents duplicate network requests from accidental double-taps.

on<PlaceOrderEvent>(
  _onPlaceOrder,
  transformer: throttle(const Duration(seconds: 2)),
);

9. Paginated lists with BasePaginationBloc #

Extend BasePaginationBloc<T> and implement the single required method fetchPage. Everything else — state management, first load, load more, and refresh — is handled for you.

class ProductsBloc extends BasePaginationBloc<Product> {
  final ProductRepository _repo;
  ProductsBloc(this._repo);

  @override
  int get pageSize => 20; // optional, default is 20

  @override
  Future<List<Product>> fetchPage(int page, int pageSize) =>
      _repo.getProducts(page: page, limit: pageSize);
}

In the UI:

// Trigger the first load (e.g. in initState):
context.read<ProductsBloc>().loadFirstPage();

// Load the next page when the user scrolls to the bottom:
context.read<ProductsBloc>().loadNextPage();

// Pull-to-refresh:
context.read<ProductsBloc>().refresh();

Building the list:

BlocBuilder<ProductsBloc, PaginationState<Product>>(
  builder: (context, state) {
    if (state.isInitial || state.isLoading) {
      return const Center(child: CircularProgressIndicator.adaptive());
    }
    if (state.isFirstPageError) {
      return DefaultErrorWidget(message: state.error!);
    }
    return ListView.builder(
      itemCount: state.items.length + (state.isLoadingMore ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == state.items.length) {
          // Bottom spinner while loading the next page
          return const Center(child: CircularProgressIndicator.adaptive());
        }
        return ProductTile(product: state.items[index]);
      },
    );
  },
)

10. BuildContext extensions #

Convenience helpers that reduce common one-liners even further.

// Dispatch an event without context.read<B>().add(...):
context.addEvent<UserBloc, UserEvent>(FetchUserEvent(id: '42'));

// Read data from SuccessState without casting:
final user = context.successData<UserBloc, User>(); // null if not SuccessState

// Reactive helpers that rebuild the widget on state change:
final user    = context.watchSuccessData<UserBloc, User>();
final loading = context.watchIsLoading<UserBloc, User>();

// Non-reactive reads (use inside callbacks, not build()):
final hasErr  = context.hasError<UserBloc, User>();
final errMsg  = context.errorMessage<UserBloc, User>();

API reference #

BaseState<T> #

Class Description
InitialState<T> The bloc has not started loading yet
LoadingState<T> An async operation is in progress
SuccessState<T>(data) Data loaded successfully, holds T data
ErrorState<T>(message) An error occurred, holds message, optional error, and showFlushbar flag

BaseBloc<E, S> #

Extends flutter_bloc's Bloc. Adds:

Future<void> executeWithErrorHandling<T>({
  required Future<T> Function() action,
  required void Function(T data) onSuccess,
  void Function(String error)? onError,
  int retries = 0,
  Duration retryDelay = const Duration(seconds: 1),
})

BaseCubit<S> #

Extends flutter_bloc's Cubit. Adds the same executeWithErrorHandling as BaseBloc, plus:

/// Emits [state] only if the cubit is not yet closed.
void safeEmit(S state)

BaseBlocObserver #

Parameter Type Default Description
onErrorCallback void Function(BlocBase, Object, StackTrace)? Called on every bloc/cubit error
logTransitions bool true Log state changes in debug mode
logEvents bool true Log incoming events in debug mode
logCreations bool false Log when blocs are created
logClosings bool false Log when blocs are closed

debounce<E>(Duration) / throttle<E>(Duration) #

Both return an EventTransformer<E> for use with Bloc.on<E>(handler, transformer: ...).

  • debounce — emits only after [duration] of silence; discards intermediate events.
  • throttle — emits the first event immediately, ignores the rest for [duration].

PaginationState<T> #

Field Type Description
items List<T> All loaded items across all pages
page int Last successfully loaded page index (0-based)
hasMore bool Whether there are more pages to load
isLoading bool true during the first page load
isLoadingMore bool true while loading a subsequent page
error String? Error message or null
isInitial bool true before any load is triggered
isFirstPageError bool true when error occurred and no items were loaded

BasePaginationBloc<T> #

Extend and implement one method:

Future<List<T>> fetchPage(int page, int pageSize);

Trigger actions via: loadFirstPage(), loadNextPage(), refresh().

Optionally override int get pageSize => 20;.


BuildContext extensions #

Method Returns Description
addEvent<B, E>(event) void Shorthand for read<B>().add(event)
currentState<B, S>() S Current state (non-reactive)
isLoading<B, T>() bool true if state is LoadingState (non-reactive)
hasError<B, T>() bool true if state is ErrorState (non-reactive)
errorMessage<B, T>() String? Error message or null (non-reactive)
successData<B, T>() T? Data from SuccessState or null (non-reactive)
watchSuccessData<B, T>() T? Reactive — rebuilds widget on change
watchIsLoading<B, T>() bool Reactive — rebuilds widget on change

BaseBlocBuilder<B, T> #

Parameter Type Description
builder Widget Function(context, T data) Required. Builds UI for SuccessState
loadingBuilder Widget Function(context)? Custom loading widget
errorBuilder Widget Function(context, String)? Custom error widget
initialBuilder Widget Function(context)? Custom initial widget
onRefresh VoidCallback? Passed to the default error widget as "retry"

BaseBlocConsumer<B, T> #

All parameters from BaseBlocBuilder, plus:

Parameter Type Default Description
showDefaultErrorFlushbar bool true Show notification automatically on ErrorState
showDefaultSuccessFlushbar bool false Show notification automatically on SuccessState
onError void Function(context, String)? Override error handling
onSuccess void Function(context, T)? Override success handling
onMessage void Function(context, String)? Custom message handling

BaseBlocListener<B, T> #

Parameter Type Default Description
child Widget Required The widget subtree to wrap
showDefaultErrorFlushbar bool true Show notification on ErrorState
showDefaultSuccessFlushbar bool false Show notification on SuccessState
onError void Function(context, String)? Override error handling
onSuccess void Function(context, T)? Override success handling

Dependencies #


4
likes
150
points
199
downloads

Publisher

unverified uploader

Weekly Downloads

Simplify BLoC usage: unified 4-state machine, auto loading/error UI, flushbar notifications, retry, pagination, debounce/throttle, safeEmit and more — all in one lightweight package.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

another_flushbar, flutter, flutter_bloc, flutter_svg

More

Packages that depend on base_flutter_bloc