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

Eliminate BLoC boilerplate. Unified 4-state machine, auto error/loading UI, built-in flushbar notifications, retry logic, pagination, debounce/throttle transformers, BaseCubit with safeEmit, BlocObser [...]

base_flutter_bloc #

A Flutter package that eliminates repetitive boilerplate when building screens with flutter_bloc. Instead of rewriting the same loading / error / success state-handling logic on every screen, you get a ready-made state machine and a set of pre-built widgets that handle everything automatically — letting you focus on what actually matters: your UI.


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 #



base_flutter_bloc — Документация на русском #

Flutter-пакет, который устраняет повторяющийся шаблонный код при создании экранов с flutter_bloc. Вместо того чтобы заново прописывать одну и ту же логику обработки состояний loading / error / success на каждом экране, вы получаете готовую машину состояний и набор виджетов, которые всё делают автоматически — оставляя вам только то, что действительно важно: ваш UI.


Проблема, которую решает этот пакет #

Типичный BLoC-экран без этого пакета выглядит так:

// ❌ До — повторяется на КАЖДОМ экране
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();
  },
);

С base_flutter_bloc тот же экран выглядит так:

// ✅ После — чисто, без шаблонного кода
BaseBlocConsumer<UserBloc, User>(
  builder: (context, user) => Text(user.name),
);

Индикатор загрузки, виджет ошибки и уведомление об ошибке — всё обрабатывается автоматически.


Возможности #

  • 🧱 BaseState<T> — унифицированная машина из 4 состояний (Initial, Loading, Success, Error) для любого типа данных
  • BaseBlocBuilder — строит UI для всех четырёх состояний с единственным обязательным параметром
  • 🔔 BaseBlocConsumer — строит UI и автоматически показывает уведомления (flushbar) при ошибке/успехе
  • 👂 BaseBlocListener — слушает изменения состояния и показывает уведомления без перестройки дерева виджетов
  • 🛠 BaseBloc — базовый класс с executeWithErrorHandling (устранение try/catch) и встроенной поддержкой повторных попыток
  • 🟦 BaseCubit — те же утилиты для Cubit-подхода, включая безопасный safeEmit()
  • 🔍 BaseBlocObserver — готовый debug-логгер и единый хук для систем мониторинга ошибок
  • debounce / throttle — готовые трансформеры событий для полей поиска и защиты от двойных нажатий
  • 📄 BasePaginationBloc — полный цикл пагинации (первая загрузка, загрузить ещё, обновить) в одном абстрактном классе
  • 🔗 Расширения BuildContextaddEvent, watchSuccessData, watchIsLoading и другие
  • 🎨 BaseFlutterBlocConfig — глобальная конфигурация для замены дефолтного виджета ошибки и уведомления на собственные реализации

Установка #

Добавьте зависимость в pubspec.yaml:

dependencies:
  base_flutter_bloc: ^<latest_version>

Затем выполните:

flutter pub get

Использование #

1. Определите свой BLoC #

Расширьте BaseBloc и эмитируйте подклассы BaseState<T>. Используйте executeWithErrorHandling, чтобы не писать try/catch каждый раз:

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)),
    );
  }
}

Никаких try/catch в каждом обработчике — executeWithErrorHandling перехватывает исключения и направляет их в onError автоматически.


2. Постройте экран #

BaseBlocBuilder — только UI

Используйте, когда нужно лишь отрисовывать разные виджеты в зависимости от состояния, без побочных эффектов.

BaseBlocBuilder<UserBloc, User>(
  // Обязательно: строит виджет когда данные загружены
  builder: (context, user) => UserCard(user: user),

  // Опциональные переопределения (у всех есть разумные дефолты):
  loadingBuilder: (context) => const MyCustomSkeletonLoader(),
  errorBuilder: (context, message) => MyErrorBanner(message: message),
  initialBuilder: (context) => const WelcomePlaceholder(),
  onRefresh: () => context.read<UserBloc>().add(FetchUserEvent()),
);
Состояние Дефолтное поведение Переопределение
InitialState SizedBox.shrink() initialBuilder
LoadingState CircularProgressIndicator.adaptive() loadingBuilder
ErrorState DefaultErrorWidget с кнопкой повтора errorBuilder
SuccessState builder (обязательно)

BaseBlocConsumer — UI + уведомления

Используйте, когда нужно одновременно строить UI и реагировать на изменения состояния (например, показывать уведомление об ошибке).

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

  // По умолчанию автоматически показывает flushbar при ErrorState.
  // Установите false, чтобы отключить:
  showDefaultErrorFlushbar: true,

  // Опционально: показывать уведомление об успехе при загрузке данных:
  showDefaultSuccessFlushbar: false,

  // Опционально: обрабатывать ошибки вручную вместо дефолтного flushbar:
  onError: (context, message) => showDialog(
    context: context,
    builder: (_) => AlertDialog(content: Text(message)),
  ),

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

BaseBlocListener — только уведомления

Используйте, когда дерево виджетов не нужно перестраивать, но нужно реагировать на переходы состояний (например, навигация после успеха).

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

3. Эмитируйте состояния из BLoC #

// Показать индикатор загрузки
emit(LoadingState());

// Передать данные в UI
emit(SuccessState(user));

// Показать виджет ошибки + уведомление flushbar
emit(ErrorState('Не удалось загрузить пользователя'));

// Показать виджет ошибки БЕЗ flushbar-уведомления
emit(ErrorState('Некритическая ошибка', showFlushbar: false));

// Прикрепить исходное исключение для отладки
emit(ErrorState('Что-то пошло не так', error: exception));

4. Глобальная настройка через BaseFlutterBlocConfig #

Оберните приложение (или любое поддерево) виджетом BaseFlutterBlocConfig, чтобы заменить дефолтный виджет ошибки и/или уведомление во всём приложении — не трогая отдельные экраны.

void main() {
  runApp(
    BaseFlutterBlocConfig(
      // Заменить DefaultErrorWidget своим дизайном
      errorWidgetBuilder: (context, message, onRetry) => MyErrorWidget(
        message: message,
        onRetry: onRetry,
      ),

      // Заменить flushbar на SnackBar, toast и т.д.
      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(),
    ),
  );
}

Если BaseFlutterBlocConfig не указан — пакет использует свои встроенные дефолты, и никакие изменения в коде не требуются.


5. BaseCubit — для более простого управления состоянием #

Когда отслеживаемые события не нужны, расширяйте BaseCubit вместо BaseBloc. Он имеет тот же метод executeWithErrorHandling и добавляет safeEmit — защиту от эмита после закрытия куbita.

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

  Future<void> fetchUser(String id) async {
    safeEmit(LoadingState());          // безопасно, даже если экран закрыт
    await executeWithErrorHandling(
      action: () => _repo.getUser(id),
      onSuccess: (user) => safeEmit(SuccessState(user)),
      onError: (msg)  => safeEmit(ErrorState(msg)),
    );
  }
}

BaseBlocBuilder, BaseBlocConsumer и BaseBlocListener работают с BaseCubit точно так же, как с BaseBloc.


6. Автоматические повторные попытки в executeWithErrorHandling #

Оба класса — BaseBloc и BaseCubit — поддерживают повторный запрос при ошибке.

await executeWithErrorHandling(
  action: () => _repo.getUser(event.id),
  onSuccess: (user) => emit(SuccessState(user)),
  onError:   (msg)  => emit(ErrorState(msg)),
  retries:     3,                          // до 3 повторных попыток
  retryDelay:  const Duration(seconds: 2), // пауза 2 с между попытками
);

Если все попытки провалились, onError вызывается один раз с сообщением последнего исключения.


7. Настройка BaseBlocObserver #

Зарегистрируйте один раз в main(), чтобы получить структурированные логи всех переходов состояний и единый хук для отправки ошибок в сервисы мониторинга.

void main() {
  Bloc.observer = BaseBlocObserver(
    // Отправка ошибок в систему мониторинга (например, Firebase Crashlytics):
    onErrorCallback: (bloc, error, stackTrace) {
      FirebaseCrashlytics.instance.recordError(error, stackTrace);
    },
    logTransitions: true,  // логировать каждое изменение состояния (только debug)
    logEvents:      true,  // логировать входящие события
    logCreations:   false, // логировать создание bloc-ов
    logClosings:    false, // логировать закрытие bloc-ов
  );
  runApp(
    BaseFlutterBlocConfig(child: const MyApp()),
  );
}

Весь вывод в консоль защищён kDebugMode — в релизных сборках не производит никакого шума.


8. Трансформеры событий debounce и throttle #

Передайте трансформер в Bloc.on<E>(), чтобы управлять скоростью обработки событий.

debounce — идеально для полей поиска

Игнорирует события, пока они продолжают поступать. Обрабатывается только последнее событие после паузы.

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 — идеально для кнопок

Пропускает первое событие немедленно, затем игнорирует последующие в течение паузы. Защищает от дублирующих запросов при двойном нажатии.

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

9. Пагинация с BasePaginationBloc #

Расширьте BasePaginationBloc<T> и реализуйте единственный обязательный метод fetchPage. Всё остальное — управление состоянием, первая загрузка, загрузка следующей страницы и обновление — берётся на себя.

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

  @override
  int get pageSize => 20; // опционально, по умолчанию 20

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

В UI:

// Запустить первую загрузку (например, в initState):
context.read<ProductsBloc>().loadFirstPage();

// Загрузить следующую страницу при прокрутке до конца:
context.read<ProductsBloc>().loadNextPage();

// Обновить список (pull-to-refresh):
context.read<ProductsBloc>().refresh();

Построение списка:

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) {
          return const Center(child: CircularProgressIndicator.adaptive());
        }
        return ProductTile(product: state.items[index]);
      },
    );
  },
)

10. Расширения BuildContext #

Вспомогательные методы, которые сокращают часто повторяющиеся паттерны.

// Отправить событие без context.read<B>().add(...):
context.addEvent<UserBloc, UserEvent>(FetchUserEvent(id: '42'));

// Прочитать данные из SuccessState без каста:
final user = context.successData<UserBloc, User>(); // null если не SuccessState

// Реактивные хелперы — перестраивают виджет при изменении состояния:
final user    = context.watchSuccessData<UserBloc, User>();
final loading = context.watchIsLoading<UserBloc, User>();

// Нереактивные (использовать в колбэках, не в build()):
final hasErr = context.hasError<UserBloc, User>();
final errMsg = context.errorMessage<UserBloc, User>();

Справочник по API #

BaseState<T> #

Класс Описание
InitialState<T> BLoC ещё не начал загрузку
LoadingState<T> Асинхронная операция выполняется
SuccessState<T>(data) Данные загружены успешно, содержит T data
ErrorState<T>(message) Произошла ошибка, содержит message, опциональный error и флаг showFlushbar

BaseBloc<E, S> #

Расширяет Bloc из flutter_bloc. Добавляет:

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> #

Расширяет Cubit из flutter_bloc. Добавляет тот же executeWithErrorHandling, плюс:

/// Эмитирует [state] только если cubit ещё не закрыт.
void safeEmit(S state)

BaseBlocObserver #

Параметр Тип Дефолт Описание
onErrorCallback void Function(BlocBase, Object, StackTrace)? Вызывается при каждой ошибке
logTransitions bool true Логировать изменения состояния
logEvents bool true Логировать входящие события
logCreations bool false Логировать создание bloc-ов
logClosings bool false Логировать закрытие bloc-ов

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

Оба возвращают EventTransformer<E> для использования с Bloc.on<E>(handler, transformer: ...).

  • debounce — эмитирует только после [duration] тишины; промежуточные события отбрасываются.
  • throttle — первое событие проходит немедленно, остальные игнорируются в течение [duration].

PaginationState<T> #

Поле Тип Описание
items List<T> Все загруженные элементы по всем страницам
page int Индекс последней успешно загруженной страницы (0-based)
hasMore bool Есть ли ещё страницы
isLoading bool true при загрузке первой страницы
isLoadingMore bool true при загрузке следующих страниц
error String? Сообщение об ошибке или null
isInitial bool true до первой загрузки
isFirstPageError bool true если ошибка при пустом списке

BasePaginationBloc<T> #

Расширьте и реализуйте один метод:

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

Действия: loadFirstPage(), loadNextPage(), refresh().

Опционально: int get pageSize => 20;.


Расширения BuildContext #

Метод Возвращает Описание
addEvent<B, E>(event) void Сокращение для read<B>().add(event)
currentState<B, S>() S Текущее состояние (не реактивно)
isLoading<B, T>() bool true если state — LoadingState
hasError<B, T>() bool true если state — ErrorState
errorMessage<B, T>() String? Текст ошибки или null
successData<B, T>() T? Данные из SuccessState или null
watchSuccessData<B, T>() T? Реактивно — перестраивает виджет
watchIsLoading<B, T>() bool Реактивно — перестраивает виджет

BaseBlocBuilder<B, T> #

Параметр Тип Описание
builder Widget Function(context, T data) Обязательно. Строит UI для SuccessState
loadingBuilder Widget Function(context)? Кастомный виджет загрузки
errorBuilder Widget Function(context, String)? Кастомный виджет ошибки
initialBuilder Widget Function(context)? Кастомный начальный виджет
onRefresh VoidCallback? Передаётся в дефолтный виджет ошибки как кнопка «повторить»

BaseBlocConsumer<B, T> #

Все параметры BaseBlocBuilder, плюс:

Параметр Тип Дефолт Описание
showDefaultErrorFlushbar bool true Автоматически показывать уведомление при ErrorState
showDefaultSuccessFlushbar bool false Автоматически показывать уведомление при SuccessState
onError void Function(context, String)? Переопределить обработку ошибок
onSuccess void Function(context, T)? Переопределить обработку успеха
onMessage void Function(context, String)? Кастомная обработка сообщений

BaseBlocListener<B, T> #

Параметр Тип Дефолт Описание
child Widget Обязательно Дочернее дерево виджетов
showDefaultErrorFlushbar bool true Показывать уведомление при ErrorState
showDefaultSuccessFlushbar bool false Показывать уведомление при SuccessState
onError void Function(context, String)? Переопределить обработку ошибок
onSuccess void Function(context, T)? Переопределить обработку успеха

Зависимости #

4
likes
0
points
201
downloads

Publisher

unverified uploader

Weekly Downloads

Eliminate BLoC boilerplate. Unified 4-state machine, auto error/loading UI, built-in flushbar notifications, retry logic, pagination, debounce/throttle transformers, BaseCubit with safeEmit, BlocObserver, and BuildContext extensions — all in one package.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

another_flushbar, flutter, flutter_bloc, flutter_svg

More

Packages that depend on base_flutter_bloc