blocx_core
Composable, Pure-Dart BLoC Architecture for Paginated Collections, Reactive Forms, Use-Case Orchestration & Real-Time Event Sync
Why blocx_core? • BlocX Ecosystem • Installation • EventHub Sync • Collection BLoC • Form BLoC • flutter_blocx UI →
Why blocx_core?
Real-world applications rarely become hard to maintain because domain rules are complex—they become hard to maintain because every screen quietly rebuilds the same state-management plumbing:
- Paginated loading, infinite scrolling, pull-to-refresh, and debounced search
- Multi-selection, row expansion, item highlighting, and optimistic/remote deletion
- Immutable form state, per-field/on-submit validation, async uniqueness checks, and multi-step wizards
- Keeping open list and form screens synchronized when an entity is created, updated, or deleted elsewhere
- Routing errors and side effects (snackbars, full-page errors, back navigation) without coupling BLoCs to Flutter
BuildContext
blocx_core turns all of that recurring plumbing into composable, pure-Dart BLoC mixins and typed UseCase tasks.
Before vs. After
| Traditional BLoC (~350+ lines per screen) | With blocx_core (~25 lines) |
|---|---|
|
|
The BlocX Ecosystem: Better Together
blocx_core is the pure-Dart domain and state layer of the BlocX ecosystem. For Flutter apps, pair it with flutter_blocx—the official Flutter UI companion package that connects directly to your blocx_core BLoCs with zero BlocConsumer, ScrollController, or TextEditingController boilerplate.
| Layer | Package | What It Gives You |
|---|---|---|
| Domain & State (Pure Dart) | blocx_core (you are here) |
BlocxCollectionBloc, BlocxFormBloc, 15+ composable mixins, 35+ validators, BlocxBaseUseCase, BlocxEventHub live sync, ScreenManagerCubit |
| Presentation & Widgets (Flutter) | flutter_blocx |
BlocxCollectionWidget, BlocxFormWidget, InfiniteList / InfiniteGrid / AnimatedInfiniteList, BlocxCollectionItem, BlocxFormTextField, BlocxFormDropdown, BlocxScreenManagerState, BlocxErrorWidget, ConfirmActionWidget |
Building a Flutter app? Install both
blocx_coreandflutter_blocxtogether to get a complete, end-to-end architecture from domain UseCases all the way to animated lists, grids, and validated forms.
Feature Highlights
📦 Collections & Paginated Lists (package:blocx_core/collection_bloc.dart)
- Offset/Limit Pagination & Infinite Scroll: Built-in
BlocxPage<T>tracking andBlocxCollectionInfiniteMixin. - Debounced Search & Dynamic Filters:
BlocxCollectionSearchableMixinandBlocxCollectionFilterMixinwith automatic page resets. - Interactive Item States: Single/multi-selection (
SelectableMixin), row expansion (ExpandableMixin), temporary row highlighting (HighlightableMixin), and programmatic scroll-to-item (ScrollableMixin). - Single & Bulk Deletion: Local or remote deletion with per-item loading indicators (
DeletableMixin). - Live EventHub Sync: Automatically insert, update, or remove items in real time when UseCases broadcast CRUD commands (
SyncStreamMixin).
📝 Reactive Forms & Validation (package:blocx_core/form_bloc.dart)
- Strongly Typed Form Entities: Immutable
BlocxBaseFormEntity<F, E>keyed by a fieldenum. - 4 Validation Modes:
none,onSubmit,onUserInteraction, andalways. - 35+ Built-in Validators: Ready-made validators for
String,int,double,DateTime,List,File,Phone, and cross-field matching. - Advanced Form Mixins: Debounced server-side uniqueness checks (
BlocxUniqueFieldValidatorMixin), reference data prefetching (BlocxFormPrefetchMixin), multi-step wizards (BlocxFormSteppedMixin), and live entity sync (BlocxFormSyncStreamMixin).
⚡ UseCases, EventHub & Screen Side Effects (package:blocx_core/blocx_core.dart)
- Standardized UseCases:
BlocxBaseUseCase,BlocxPaginatedUseCase, andBlocxSearchUseCasewith automatic exception-to-BlocxUseCaseResultconversion. - Unidirectional Command EventHub: UseCases broadcast
BlocxCommandType(create,read,update,delete) events toBlocxEventHub; BLoCs subscribe and update their states automatically. - UI-Agnostic Side Effects:
ScreenManagerCubitlets pure-Dart BLoCs trigger snackbars, full-page errors, and navigation pops without importing Flutter.
Installation
Add blocx_core to your pubspec.yaml:
dependencies:
blocx_core: ^1.0.0
Or via the CLI:
dart pub add blocx_core
Using Flutter? Add
flutter_blocxalongsideblocx_core:flutter pub add blocx_core flutter_blocx
Barrel Imports
Import only what your file needs:
// 1. Core: Entities, UseCases, Tasks, EventHub, ScreenManagerCubit, Error Translation, Localization
import 'package:blocx_core/blocx_core.dart';
// 2. Collections: BlocxCollectionBloc, Collection Events/States, BlocxPage, and all 10 Collection Mixins
import 'package:blocx_core/collection_bloc.dart';
// 3. Forms: BlocxFormBloc, Form Events/States, BlocxBaseFormEntity, Validators, and all 5 Form Mixins
import 'package:blocx_core/form_bloc.dart';
Architecture Overview
┌──────────────────────────────────────────────────────────────────────┐
│ UseCases (Domain) │
│ BlocxBaseUseCase / BlocxPaginatedUseCase / BlocxSearchUseCase │
└───────────────┬──────────────────────────────────────┬───────────────┘
│ returns BlocxUseCaseResult │ broadcasts CRUD commands
▼ ▼
┌───────────────────────────────────┐ ┌─────────────────────────────┐
│ Your BLoC (State Layer) │ │ BlocxEventHub │
│ BlocxCollectionBloc + Mixins │◄───│ create / read / update / │
│ BlocxFormBloc + Mixins │ │ delete BlocxEntityEvents │
└───────────────┬───────────────────┘ └─────────────────────────────┘
│ emits state & ScreenManagerCubit intents
▼
┌──────────────────────────────────────────────────────────────────────┐
│ UI Layer (flutter_blocx) │
│ BlocxCollectionWidget / BlocxFormWidget / BlocxScreenManagerState │
└──────────────────────────────────────────────────────────────────────┘
Core Concepts: Entities, UseCases & Live EventHub Sync
1. Domain Entities (BlocxBaseEntity)
Every domain model managed by a collection or broadcasted through BlocxEventHub extends BlocxBaseEntity and provides a unique identifier:
import 'package:blocx_core/blocx_core.dart';
class ProductEntity extends BlocxBaseEntity {
final String id;
final String name;
final double price;
final bool isAvailable;
const ProductEntity({
required this.id,
required this.name,
required this.price,
this.isAvailable = true,
});
@override
String get identifier => id;
}
2. UseCases & Automatic BlocxEventHub Command Broadcasting
In blocx_core, only UseCases emit app-wide domain events—BLoCs never emit them.
When you pass an eventHub and commandType (or commandTypes) to a BlocxBaseUseCase, calling await useCase.execute(input) automatically broadcasts BlocxEntityEvents for every affected entity as soon as perform(input) succeeds:
import 'package:blocx_core/blocx_core.dart';
import 'package:blocx_core/collection_bloc.dart';
// Paginated Read UseCase (defaults to BlocxCommandType.read)
class LoadProductsUseCase extends BlocxPaginatedUseCase<BlocxPaginatedInput, ProductEntity> {
final ProductRepository repository;
LoadProductsUseCase(this.repository, {super.eventHub});
@override
Future<BlocxUseCaseResult<BlocxPage<ProductEntity>>> perform(
BlocxPaginatedInput input,
) async {
final items = await repository.fetchPage(offset: input.offset, limit: input.limit);
return success(BlocxPage(items: items, offset: input.offset, limit: input.limit));
}
}
// Create / Update UseCase (Output is ProductEntity -> auto-resolved for EventHub)
class SaveProductUseCase extends BlocxBaseUseCase<ProductEntity, ProductEntity> {
final ProductRepository repository;
SaveProductUseCase(this.repository, {required bool isCreate, super.eventHub})
: super(
commandType: isCreate ? BlocxCommandType.create : BlocxCommandType.update,
);
@override
Future<BlocxUseCaseResult<ProductEntity>> perform(ProductEntity input) async {
final saved = await repository.save(input);
return success(saved);
}
}
// Delete UseCase (Input is ProductEntity, Output is bool -> auto-resolved from Input)
class DeleteProductUseCase extends BlocxBaseUseCase<ProductEntity, bool> {
final ProductRepository repository;
DeleteProductUseCase(this.repository, {super.eventHub})
: super(commandType: BlocxCommandType.delete);
@override
Future<BlocxUseCaseResult<bool>> perform(ProductEntity input) async {
await repository.delete(input.id);
return success(true);
}
}
Tip: A single UseCase can also emit multiple commands by passing
commandTypes: const [BlocxCommandType.update, BlocxCommandType.read]tosuper(...), or customize entity extraction by overridingresolveCommandEntities(input, output, command).
Collection BLoC
BlocxCollectionBloc<Entity, Payload> orchestrates list/grid state. Compose it with any of the 10 built-in collection mixins:
| Mixin | Capability Added |
|---|---|
BlocxCollectionInfiniteMixin<T, P> |
Infinite scrolling (paginationTask / loadNextPageTask) |
BlocxCollectionRefreshableMixin<T, P> |
Pull-to-refresh list reload (refreshTask) |
BlocxCollectionSearchableMixin<T, P> |
Debounced search & paginated search results (searchUseCaseTask) |
BlocxCollectionFilterMixin<T, P, F> |
Strongly typed filter state (currentFilter) with automatic reload |
BlocxCollectionSelectableMixin<T, P> |
Single/multi-selection (selectedItems) with optional remote sync |
BlocxCollectionDeletableMixin<T, P> |
Single & bulk deletion (deleteItemTask, deleteMultipleItemsTask) |
BlocxCollectionHighlightableMixin<T, P> |
Row highlight state (highlightedItems) |
BlocxCollectionExpandableMixin<T, P> |
Single or multi-row expansion (expandedItems) |
BlocxCollectionScrollableMixin<T, P> |
Programmatic scroll-to-item with optional highlight after scroll |
BlocxCollectionSyncStreamMixin<T, P> |
Real-time list updates from BlocxEventHub or custom entity streams |
Quickstart: Paginated, Searchable, Deletable & Live-Synced Collection BLoC
import 'package:blocx_core/blocx_core.dart';
import 'package:blocx_core/collection_bloc.dart';
class ProductsCollectionBloc extends BlocxCollectionBloc<ProductEntity, void>
with
BlocxCollectionInfiniteMixin<ProductEntity, void>,
BlocxCollectionRefreshableMixin<ProductEntity, void>,
BlocxCollectionSearchableMixin<ProductEntity, void>,
BlocxCollectionSelectableMixin<ProductEntity, void>,
BlocxCollectionDeletableMixin<ProductEntity, void>,
BlocxCollectionSyncStreamMixin<ProductEntity, void> {
final LoadProductsUseCase loadProductsUseCase;
final BlocxSearchUseCase<BlocxSearchInput, ProductEntity> searchProductsUseCase;
final DeleteProductUseCase deleteProductUseCase;
@override
final BlocxEventHub eventHub;
ProductsCollectionBloc({
required this.loadProductsUseCase,
required this.searchProductsUseCase,
required this.deleteProductUseCase,
required this.eventHub,
}) : super();
// Shared pagination task used by initial load, infinite scroll, and pull-to-refresh
@override
BlocxPaginatedUseCaseTask<BlocxPaginatedInput, ProductEntity>? get paginationTask {
return BlocxPaginatedUseCaseTask<BlocxPaginatedInput, ProductEntity>(
useCase: loadProductsUseCase,
inputBuilder: (offset, limit) => BlocxPaginatedInput(offset: offset, limit: limit),
);
}
@override
BlocxPaginatedUseCaseTask<BlocxSearchInput, ProductEntity>? get searchUseCaseTask {
return BlocxPaginatedUseCaseTask<BlocxSearchInput, ProductEntity>(
useCase: searchProductsUseCase,
inputBuilder: (offset, limit) => BlocxSearchInput(
searchText: searchText,
offset: offset,
limit: limit,
),
);
}
@override
BlocxUseCaseTask<Object?, bool>? deleteItemTask(ProductEntity item) {
return BlocxUseCaseTask<ProductEntity, bool>(
useCase: deleteProductUseCase,
inputBuilder: () => item,
);
}
// Optional filter for live EventHub synchronization:
@override
bool shouldSyncEntity(ProductEntity entity, BlocxCommandType command) {
if (command == BlocxCommandType.delete) return true;
return entity.isAvailable;
}
}
Form BLoC
BlocxFormBloc<F, P, E> manages form data (F extends BlocxBaseFormEntity<F, E>), edit-mode hydration payload (P), and field keys (E extends Enum). Compose it with any of the 5 built-in form mixins:
| Mixin | Capability Added |
|---|---|
BlocxFormValidationMixin<F, P, E> |
Per-field and full-form validation (none, onSubmit, onUserInteraction, always) |
BlocxUniqueFieldValidatorMixin<F, P, E> |
Debounced async uniqueness checks per field (e.g. username/email availability) |
BlocxFormPrefetchMixin<F, P, E> |
Prefetch remote reference data (e.g. dropdown options) before form interaction |
BlocxFormSteppedMixin<F, P, E> |
Multi-step wizard progression (currentStep, totalSteps) |
BlocxFormSyncStreamMixin<F, P, E, WatchedEntity> |
Live form updates when the watched entity is modified—and auto-pop() if deleted |
Quickstart: Validated Edit Form BLoC with Live Sync
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:blocx_core/blocx_core.dart';
import 'package:blocx_core/form_bloc.dart';
enum ProductFormField { name, price }
class ProductFormEntity extends BlocxBaseFormEntity<ProductFormEntity, ProductFormField> {
final String id;
final String name;
final double price;
const ProductFormEntity({this.id = 'new', this.name = '', this.price = 0.0});
@override
String get identifier => id;
ProductFormEntity copyWith({String? id, String? name, double? price}) =>
ProductFormEntity(
id: id ?? this.id,
name: name ?? this.name,
price: price ?? this.price,
);
@override
ProductFormEntity updateByKey(ProductFormField key, dynamic value) => switch (key) {
ProductFormField.name => copyWith(name: value as String? ?? ''),
ProductFormField.price => copyWith(price: (value as num?)?.toDouble() ?? 0.0),
};
@override
dynamic getValueByKey(ProductFormField key) => switch (key) {
ProductFormField.name => name,
ProductFormField.price => price,
};
@override
String? getFormattedValueByKey(ProductFormField key) => getValueByKey(key)?.toString();
}
class ProductFormValidator extends BlocxFormValidator<ProductFormEntity, ProductFormField> {
@override
List<ProductFormField> formKeys() => ProductFormField.values;
@override
List<BlocxFieldValidator<ProductFormEntity, ProductFormField, dynamic>> getValidatorsByKey(
ProductFormEntity formData,
ProductFormField key,
) =>
switch (key) {
ProductFormField.name => [
BlocxStringRequiredValidator(),
const BlocxStringMinLengthValidator(3),
],
ProductFormField.price => [
BlocxDoubleRequiredValidator(),
BlocxDoublePositiveValidator(),
],
};
}
class ProductFormBloc
extends BlocxFormBloc<ProductFormEntity, ProductEntity, ProductFormField>
with
BlocxFormValidationMixin<ProductFormEntity, ProductEntity, ProductFormField>,
BlocxFormSyncStreamMixin<ProductFormEntity, ProductEntity, ProductFormField,
ProductEntity> {
final SaveProductUseCase saveProductUseCase;
@override
final BlocxEventHub eventHub;
@override
final BlocxFormValidator<ProductFormEntity, ProductFormField> validator =
ProductFormValidator();
ProductFormBloc({
required this.saveProductUseCase,
required this.eventHub,
}) : super(const ProductFormEntity());
@override
List<ProductFormField> get formKeysList => ProductFormField.values;
@override
FormValidationMode get formValidationMode => FormValidationMode.onUserInteraction;
@override
FutureOr<ProductFormEntity> applyPayloadToFormData(ProductEntity payload) {
return formData.copyWith(id: payload.id, name: payload.name, price: payload.price);
}
@override
BlocxUseCaseTask<Object?, Object?> get submitUseCaseTask {
return BlocxUseCaseTask<ProductEntity, ProductEntity>(
useCase: saveProductUseCase,
inputBuilder: () => ProductEntity(
id: formData.id,
name: formData.name,
price: formData.price,
),
);
}
@override
FutureOr<bool> onFormSubmitted(
Emitter<BlocxFormState<ProductFormEntity, ProductFormField>> emit,
BlocxUseCaseResult<Object?> result,
) {
displayInfoSnackbar('Product saved successfully!');
return true;
}
@override
FutureOr<ProductFormEntity?> mapSyncedEntityToFormData(
ProductEntity entity,
BlocxCommandType command,
) {
return formData.copyWith(id: entity.id, name: entity.name, price: entity.price);
}
}
Built-in Field Validators (package:blocx_core/form_bloc.dart)
- String:
BlocxStringRequiredValidator,BlocxStringMinLengthValidator(minLength),BlocxStringMaxLengthValidator(maxLength),BlocxStringLengthRangeValidator(minLength: ..., maxLength: ...),BlocxStringExactLengthValidator(length),BlocxStringEmailValidator,BlocxStringNumericValidator,BlocxStringAlphanumericValidator,BlocxStringUrlValidator,BlocxStringMatchValidator(otherKey) - Integer:
BlocxIntegerRequiredValidator,BlocxIntegerMinValueValidator(minValue),BlocxIntegerMaxValueValidator(maxValue),BlocxIntegerPositiveValidator,BlocxIntegerNonZeroValidator,BlocxIntegerRangeValidator(minValue, maxValue),BlocxIntegerGreaterThanFieldValidator(otherKey),BlocxIntegerLessThanFieldValidator(otherKey) - Double:
BlocxDoubleRequiredValidator,BlocxDoubleMinValueValidator(minValue),BlocxDoubleMaxValueValidator(maxValue),BlocxDoublePositiveValidator,BlocxDoubleRangeValidator(minValue, maxValue) - DateTime:
BlocxDateTimeRequiredValidator,BlocxDateTimeMinValidator(minDate),BlocxDateTimeMaxValidator(maxDate),BlocxDateTimeRangeValidator(minDate: ..., maxDate: ...),BlocxDateTimeAfterFieldValidator(otherKey),BlocxDateTimeBeforeFieldValidator(otherKey) - Phone:
BlocxPhoneRequiredValidator,BlocxPhoneBasicFormatValidator,BlocxPhoneE164Validator,BlocxPhoneMinLengthValidator(minDigits),BlocxPhoneMaxLengthValidator(maxDigits) - List, File & Object:
BlocxListRequiredValidator,BlocxListMinItemsValidator(minItems),BlocxListMaxItemsValidator(maxItems),BlocxListUniqueItemsValidator,BlocxFileRequiredValidator,BlocxFileMaxSizeValidator(maxBytes),BlocxRequiredFieldValidator
Screen Side Effects, Error Translation & Localization
Every BlocxBaseBloc (including BlocxCollectionBloc and BlocxFormBloc) owns an internal ScreenManagerCubit to trigger UI side effects cleanly from pure Dart:
// Inside any BlocxBaseBloc subclass:
displayInfoSnackbar('Saved!', title: 'Success');
displayWarningSnackbar('Connection is slow');
displayErrorSnackbar('Could not delete item');
displayErrorWidget(ReadableError(title: 'Offline', message: 'Check your connection'));
pop(); // Instructs the UI screen to pop the current route
Customize global error translation and localization at app startup:
BlocxErrorTranslator.instance = MyCustomErrorTranslator();
BlocXLocalizations.localizations = MyCustomLocalizations();
Render Your BLoCs in Flutter with flutter_blocx
Don't write repetitive BlocConsumer, ScrollController, or TextEditingController glue code in Flutter! Pair blocx_core with flutter_blocx (GitHub):
BlocxCollectionWidget&BlocxCollectionWidgetState: Automatically wiresProductsCollectionBlocto animated lists, infinite grids, slivers, pull-to-refresh,BlocxSearchField, andBlocxCollectionItemcards.BlocxFormWidget&BlocxFormWidgetState: Automatically managesTextEditingControllers andFocusNodes, bindstextField(),dropdown(),checkbox(), andBlocxFormButtonRow, and handles edit-mode hydration.BlocxScreenManagerState: Automatically listens toScreenManagerCubitto showBlocxSnackBar, renderBlocxErrorWidgetwith retry callbacks, and pop routes.
👉 Explore flutter_blocx on pub.flutter-io.cn
Included AI Coding Skill
This repository includes an AI agent skill at skills/blocx-core/SKILL.md (compatible with Claude Code, Antigravity, and Cursor) containing full architectural rules, blueprints, and progressive-disclosure reference guides for blocx_core.
Contributing & License
Contributions, issues, and feature requests are welcome at the issue tracker.
Released under the MIT License.
Libraries
- blocx_core
- Core primitives, base entities, use cases, error handling, and event hub for the BlocX architecture.
- collection_bloc
- form_bloc