blocx_core 0.8.0 copy "blocx_core: ^0.8.0" to clipboard
blocx_core: ^0.8.0 copied to clipboard

Composable BLoC mixins and use-cases for lists & forms (Dart-only core).

blocx_core logo

blocx_core

Composable BLoC building blocks for lists and forms in pure Dart.
Framework-agnostic. Minimal boilerplate. Maximum control.

pub version pub points platform license


Overview #

blocx_core is a Dart-only library that provides composable, mixin-based primitives for building BLoC-pattern state management layers in Dart and Flutter applications.

Rather than shipping monolithic blocs that bundle every feature together, blocx_core lets you opt in to only the capabilities you need — infinite scrolling, search, pull-to-refresh, selection, expansion, highlight, and more — by mixing lightweight, focused mixins into your own domain blocs.

Framework-agnostic. blocx_core has no Flutter dependency. Pair it with flutter_blocx for ready-made UI widgets built on top of this core.


Table of Contents #


Installation #

Add blocx_core to your pubspec.yaml:

dependencies:
  blocx_core: ^0.8.0

Or install via the command line:

dart pub add blocx_core
# Inside a Flutter project:
flutter pub add blocx_core

Import the library:

import 'package:blocx_core/blocx_core.dart';
// For form-specific types:
import 'package:blocx_core/form_bloc.dart';

Requirements: Dart SDK >=3.5.0


Architecture Overview #

blocx_core is organised around three pillars:

┌─────────────────────────────────────────────────┐
│                  Your Domain BLoC                │
│  extends BlocxListBloc / BlocxFormBloc           │
│  with  <only the mixins you need>                │
└───────────────────┬─────────────────────────────┘
                    │ delegates async work to
┌───────────────────▼─────────────────────────────┐
│               Use Cases                          │
│  BlocxBaseUseCase → UseCaseResult<T>             │
│  BlocxPaginationUseCase / SearchUseCase          │
└───────────────────┬─────────────────────────────┘
                    │ UI intents via
┌───────────────────▼─────────────────────────────┐
│           ScreenManagerCubit                     │
│  Emits snackbar / error-page / pop intents       │
│  UI layer decides how to render them             │
└─────────────────────────────────────────────────┘

Core Concepts #

BaseEntity #

All domain objects used with list blocs must extend BaseEntity. It provides stable identity and equality semantics based on a unique id.

class Product extends BaseEntity {
  @override
  final String id;

  final String name;
  final double price;

  const Product({required this.id, required this.name, required this.price});
}

The identifier getter (also on BaseEntity) is used internally for scroll-to operations.


UseCase & UseCaseResult #

Every piece of async business logic is encapsulated in a BlocxBaseUseCase<T> subclass. Use cases return a UseCaseResult<T>, which is either a success carrying data or a failure carrying an error and optional stack trace.

class FetchProducts extends BlocxBaseUseCase<List<Product>> {
  final ProductRepository repo;
  FetchProducts({required this.repo});

  @override
  Future<UseCaseResult<List<Product>>> perform() async {
    try {
      final data = await repo.getAll();
      return UseCaseResult.success(data);
    } catch (e, s) {
      return UseCaseResult.failure(e, stackTrace: s);
    }
  }
}

For paginated data, extend BlocxPaginationUseCase<T> (which adds loadCount and offset parameters) or SearchUseCase<T> (which additionally provides searchText).


Page<T> #

Page<T> is the normalized container for a page of items returned by pagination use cases. It carries the list of items and signals whether the end of the data source has been reached.

// successResult() is a helper on BlocxPaginationUseCase that
// wraps a List<T> into a Page<T> automatically.
return successResult(items);

// To signal the last page:
return successResult(items, isLastPage: true);

BlocxListBloc #

BlocxListBloc<T, P> is the central class for list state management, where T is your entity type and P is an optional payload type passed when loading the initial page (use void if no payload is needed).

Extend it and compose only the mixins you require. Each mixin is initialized via a corresponding init*() call in the constructor.


BlocxFormBloc #

BlocxFormBloc<F, P, E> manages a form backed by a BlocxBaseFormEntity subclass (F), an optional initialization payload (P), and an enum (E) that enumerates the form's fields.


ScreenManagerCubit #

ScreenManagerCubit is owned and managed internally by BaseBloc — you no longer need to construct or pass one explicitly. Simply call super(initialState) in your bloc's constructor:

class CounterBloc extends BaseBloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterStateInitial());
}

ScreenManagerCubit acts as a communication channel between your BLoC layer and the presentation layer. Instead of importing Flutter from within a BLoC, you emit typed intents that the UI listens to and renders.

Available intent methods (callable from any bloc):

Method Emitted State
displaySnackBar(...) ScreenManagerCubitStateDisplaySnackbar
displayErrorWidget(...) ScreenManagerCubitStateDisplayErrorPage
displayErrorWidgetByErrorCode(...) ScreenManagerCubitStateDisplayErrorPageByErrorCode
pop() ScreenManagerCubitStatePop

List BLoC #

Available List Mixins #

Mix these into your BlocxListBloc subclass. Call the corresponding init*() method in your constructor.

Mixin init call Capability
BlocxCollectionInfiniteMixin initInfiniteList() Next-page loading, reached-end flag, scroll-triggered pagination
BlocxCollectionSearchableMixin initSearchable() Debounced search, search-next-page, search-refresh
BlocxCollectionRefreshableMixin initRefresh() Pull-to-refresh semantics
BlocxCollectionSelectableMixin initSelectable() Single and multi-item selection and deselection
BlocxCollectionHighlightableMixin (auto) Highlight and clear-highlight on individual items
BlocxCollectionExpandableMixin (auto) Expand, collapse, and toggle expansion on individual items
BlocxCollectionScrollableMixin (auto) Programmatic scroll-to-item and scroll-to-identifier
BlocxCollectionDeletableMixin (auto) Remove single items, remove by ID, remove multiple items
BlocxCollectionSyncStreamMixin (auto) Sync list state from an external stream

Available List Events #

Event Description
BlocxListEventLoadInitialPage<T, P> Load the first page of data
BlocxListEventLoadNextPage<T> Append the next page to the existing list
BlocxListEventRefreshData<T> Reload the list from the source
BlocxListEventSearch<T> Run a debounced search query
BlocxListEventSearchNextPage<T> Load the next page of search results
BlocxListEventSearchRefresh<T> Refresh the current search results
BlocxListEventClearSearch<T> Clear search and restore the base list
BlocxListEventSelectItem<T> Select a single item
BlocxListEventDeselectItem<T> Deselect a single item
BlocxListEventSelectMultipleItems<T> Select multiple items at once
BlocxListEventDeselectMultipleItems<T> Deselect multiple items at once
BlocxListEventClearSelection<T> Clear all selections
BlocxListEventHighlightItem<T> Highlight a specific item
BlocxListEventClearHighlightedItem<T> Clear the highlight on an item
BlocxListEventExpandItem<T> Expand an item's details
BlocxListEventCollapseItem<T> Collapse an item's details
BlocxListEventToggleItemExpansion<T> Toggle expansion state of an item
BlocxListEventScrollToItem<T> Scroll to a given item
BlocxListEventScrollToIdentifier<T> Scroll to an item by its identifier
BlocxListEventAddItem<T> Insert an item into the list
BlocxListEventUpdateItem<T> Replace an item in the list
BlocxListEventRemoveItem<T> Remove a single item
BlocxListEventRemoveItemById<T> Remove an item by its ID
BlocxListEventRemoveMultipleItems<T> Remove multiple items at once
BlocxListEventReplaceList<T> Replace the entire list

Available List States #

State Description
BlocxListStateLoading<T> Initial load or refresh in progress
BlocxListStateLoaded<T> Data is available
BlocxListStateError<T> An error occurred while loading
BlocxListStateSelectionChanged<T> Selection has been updated
BlocxListStateScrollToItem<T> Scroll-to intent emitted

Use the ListStateExtensions extension on BlocxListState<T> for convenience accessors.


Form BLoC #

BaseFormEntity #

Your form's data model must extend BlocxBaseFormEntity<F, E>, where F is the form entity itself and E is an enum enumerating the form's fields. The entity must be immutable and implement copyWith.

enum ProfileField { name, email, phone }

class ProfileForm extends BlocxBaseFormEntity<ProfileForm, ProfileField> {
  final String name;
  final String email;
  final String phone;

  const ProfileForm({
    this.name = '',
    this.email = '',
    this.phone = '',
  });

  @override
  ProfileForm copyWith({String? name, String? email, String? phone}) =>
      ProfileForm(
        name: name ?? this.name,
        email: email ?? this.email,
        phone: phone ?? this.phone,
      );
}

Built-in Validators #

Validators extend BlocxFieldValidator<T> and are composed per field inside a BlocxFormValidator subclass.

Validator Description
BlocxRequiredValidator Field must not be null or empty
BlocxMinLengthValidator String must have at least N characters
BlocxMaxLengthValidator String must not exceed N characters
BlocxExactLengthValidator String must be exactly N characters
BlocxLengthRangeValidator String length within [min, max]
BlocxRegexValidator String must match a regular expression
BlocxMinValueValidator<T> Numeric value >= min
BlocxMaxValueValidator<T> Numeric value <= max
BlocxRangeValueValidator Numeric value within [min, max]
BlocxMinDateValidator DateTime not before minDate
BlocxMaxDateValidator DateTime not after maxDate
BlocxDateRangeValidator DateTime within [minDate, maxDate]
BlocxMatchFieldValidator<T> Field value must match another field's value
BlocxConditionalRequiredValidator Required only when a condition is true

Form Events #

Event Description
BlocxFormEventInit<P> Initialize the form, optionally with a payload
BlocxFormEventFetchRequiredInfo Fetch any data the form depends on before rendering
BlocxFormEventUpdateData<E> Update the value of a single field
BlocxFormEventUpdateFormData<P> Replace the entire form data object
BlocxFormEventSubmit Validate and submit the form
BlocxFormEventSetErrorToField<E> Manually set an error on a specific field
BlocxFormEventSetTimedErrorToField<E> Set a time-limited error on a field
BlocxFormEventClearFieldError<E> Clear the error on a specific field
BlocxFormEventCheckUniqueValue<E> Trigger async uniqueness check for a field
BlocxFormEventNextStep Advance to the next step (stepped forms)
BlocxFormEventPreviousStep Return to the previous step (stepped forms)
BlocxFormEventGoToStep Jump to a specific step (stepped forms)

Form States #

State Description
BlocxFormStateInitial<F, E> Form not yet initialized
BlocxFormStateLoaded<F, E> Form loaded and ready for interaction
BlocxFormStateFormUpdated<F, E> A field value or error has changed
BlocxFormStateApplyInitialDataToForm<F, E> Initial data applied to the form
BlocxFormStateSubmittingForm<F, E> Submission in progress
BlocxFormStateFormSubmitted<F, E> Submission completed successfully

Form Mixins #

Mixin Capability
BlocxFormValidationMixin Per-field and whole-form validation
BlocxFormErrorsMixin Programmatic error setting and clearing
BlocxFormInfoFetcherMixin Fetch remote data required before the form is ready
BlocxFormSteppedMixin Multi-step form navigation (next, previous, go-to)
BlocxUniqueFieldValidatorMixin Async server-side uniqueness validation per field

Use Case Tasks #

BlocxUseCaseTask and BlocxPaginatedUseCaseTask pair a use case with a lazily evaluated input builder, so input is always constructed from the latest runtime state at execution time rather than at registration time.

BlocxUseCaseTask #

BlocxUseCaseTask(
  useCase: getUserUseCase,
  inputBuilder: () => GetUserInput(id: currentUserId),
);

BlocxPaginatedUseCaseTask #

Use this as the standard task type for BlocxCollectionBloc.paginationTask. The inputBuilder receives the current limit (page size) and offset (number of already-loaded items) at execution time:

@override
BlocxPaginatedUseCaseTask get paginationTask => BlocxPaginatedUseCaseTask(
  useCase: _getOrdersUseCase,
  inputBuilder: ({required limit, required offset}) =>
      BlocxPaginationInput(limit: limit, offset: offset),
);

To include extra fields from bloc state:

@override
BlocxPaginatedUseCaseTask get paginationTask => BlocxPaginatedUseCaseTask(
  useCase: _getOrdersUseCase,
  inputBuilder: ({required limit, required offset}) => GetOrdersInput(
    limit: limit,
    offset: offset,
    userId: payload!.id,
    status: currentFilter,
  ),
);

If initial load, next-page, and refresh each hit different endpoints, override BlocxCollectionBloc.loadInitialPageTask individually instead.


Error & Screen Management #

Any bloc can emit UI intents without importing Flutter. Error handling is built into BaseBloc — call handleError from event handlers to log and surface errors via the configured errorDisplayPolicy (snackbar by default):

} catch (e, st) {
  handleError(e, emit, stacktrace: st);
}

To display a full-page error instead, override errorDisplayPolicy in your bloc:

@override
ErrorDisplayPolicy get errorDisplayPolicy => ErrorDisplayPolicy.page;

Register a BlocxErrorTranslator once at app startup to map raw exceptions to human-readable ReadableError instances — blocs pick it up automatically.

The presentation layer listens to ScreenManagerCubit and handles each intent:

// Inside a BLoC event handler:
displaySnackBar(
  message: 'Item deleted successfully.',
  type: BlocXSnackbarType.success,
);

displayErrorWidget(
  error: ReadableError(title: 'Not Found', message: 'The resource could not be loaded.'),
);

pop();
// In your Flutter widget or BlocListener:
BlocListener<ScreenManagerCubit, ScreenManagerCubitState>(
  bloc: screenCubit,
  listener: (context, state) {
    if (state is ScreenManagerCubitStateDisplaySnackbar) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    } else if (state is ScreenManagerCubitStatePop) {
      Navigator.of(context).pop();
    }
  },
);

BlocXErrorCode and BlocXSnackbarType enums give you typed control over the intent payload.


Quickstart: Paged & Searchable List #

The following example wires up a fully paginated, searchable, refreshable, and selectable list for a Todo entity.

1. Define the Entity #

import 'package:blocx_core/blocx_core.dart';

class Todo extends BaseEntity {
  @override
  final String id;
  final String title;
  final bool completed;

  const Todo({required this.id, required this.title, this.completed = false});
}

2. Define the Repository Contract #

abstract class TodoRepository {
  Future<List<Todo>> fetchPage({required int limit, required int offset});
  Future<List<Todo>> search({required String query, required int limit, required int offset});
}

3. Implement Use Cases #

class FetchTodosUseCase extends BlocxPaginationUseCase<Todo> {
  final TodoRepository repo;

  FetchTodosUseCase({
    required this.repo,
    required super.loadCount,
    required super.offset,
  });

  @override
  Future<UseCaseResult<Page<Todo>>> perform() async {
    try {
      final items = await repo.fetchPage(limit: loadCount, offset: offset);
      return successResult(items);
    } catch (e, s) {
      return UseCaseResult.failure(e, stackTrace: s);
    }
  }
}

class SearchTodosUseCase extends SearchUseCase<Todo> {
  final TodoRepository repo;

  SearchTodosUseCase({
    required this.repo,
    required super.searchText,
    required super.loadCount,
    required super.offset,
  });

  @override
  Future<UseCaseResult<Page<Todo>>> perform() async {
    try {
      final items = await repo.search(
        query: searchText,
        limit: loadCount,
        offset: offset,
      );
      return successResult(items);
    } catch (e, s) {
      return UseCaseResult.failure(e, stackTrace: s);
    }
  }
}

4. Compose the BLoC #

class TodosBloc extends BlocxListBloc<Todo, void>
    with
        BlocxCollectionInfiniteMixin<Todo, void>,
        BlocxCollectionSearchableMixin<Todo, void>,
        BlocxCollectionRefreshableMixin<Todo, void>,
        BlocxCollectionSelectableMixin<Todo, void> {
  final TodoRepository repo;

  TodosBloc({required this.repo}) : super(BlocxInfiniteListBloc()) {
    initInfiniteList();
    initSearchable();
    initRefresh();
    initSelectable();
    add(BlocxListEventLoadInitialPage<Todo, void>());
  }

  @override
  BlocxPaginationUseCase<Todo>? get loadInitialPageUseCase =>
      FetchTodosUseCase(repo: repo, loadCount: 20, offset: 0);

  @override
  BlocxPaginationUseCase<Todo>? get loadNextPageUseCase =>
      FetchTodosUseCase(repo: repo, loadCount: 20, offset: list.length);

  @override
  BlocxPaginationUseCase<Todo>? get refreshPageUseCase =>
      FetchTodosUseCase(repo: repo, loadCount: list.length, offset: 0);

  @override
  SearchUseCase<Todo>? searchUseCase(String q, {int? loadCount, int? offset}) =>
      SearchTodosUseCase(
        repo: repo,
        searchText: q,
        loadCount: loadCount ?? 20,
        offset: offset ?? 0,
      );

  @override
  (String, String?) convertErrorToMessageAndTitle(Object error) =>
      ('Failed to load todos. Please try again.', null);
}

Note: ScreenManagerCubit is now owned internally by BaseBloc. The screen parameter has been removed from the constructor — just call super(initialState).

5. Drive the BLoC #

final bloc = TodosBloc(repo: myRepo);

// Pagination
bloc.add(BlocxListEventLoadNextPage<Todo>());

// Search
bloc.add(BlocxListEventSearch<Todo>(searchText: 'urgent'));
bloc.add(BlocxListEventClearSearch<Todo>());

// Selection
bloc.add(BlocxListEventSelectItem<Todo>(item: someTodo));
bloc.add(BlocxListEventClearSelection<Todo>());

// Refresh
bloc.add(BlocxListEventRefreshData<Todo>());

Quickstart: Form with Validation #

1. Define the Field Enum and Form Entity #

import 'package:blocx_core/form_bloc.dart';

enum SignUpField { email, password, confirmPassword }

class SignUpForm extends BlocxBaseFormEntity<SignUpForm, SignUpField> {
  final String email;
  final String password;
  final String confirmPassword;

  const SignUpForm({
    this.email = '',
    this.password = '',
    this.confirmPassword = '',
  });

  @override
  SignUpForm copyWith({
    String? email,
    String? password,
    String? confirmPassword,
  }) =>
      SignUpForm(
        email: email ?? this.email,
        password: password ?? this.password,
        confirmPassword: confirmPassword ?? this.confirmPassword,
      );
}

2. Define the Validator #

class SignUpValidator extends BlocxFormValidator<SignUpForm, SignUpField> {
  @override
  Map<SignUpField, List<BlocxFieldValidator>> get validators => {
        SignUpField.email: [
          BlocxRequiredValidator(),
          BlocxRegexValidator(
            pattern: r'^[^@]+@[^@]+\.[^@]+$',
            errorMessage: 'Enter a valid email address.',
          ),
        ],
        SignUpField.password: [
          BlocxRequiredValidator(),
          BlocxMinLengthValidator(minLength: 8),
        ],
        SignUpField.confirmPassword: [
          BlocxRequiredValidator(),
          BlocxMatchFieldValidator<String>(
            otherFieldValue: (form) => form.password,
            errorMessage: 'Passwords do not match.',
          ),
        ],
      };
}

3. Implement the FormBloc #

class SignUpBloc extends BlocxFormBloc<SignUpForm, void, SignUpField>
    with BlocxFormValidationMixin<SignUpForm, void, SignUpField> {
  SignUpBloc() : super(const SignUpForm(), SignUpValidator());

  @override
  Future<void> onSubmit(SignUpForm form) async {
    // Perform submission logic, e.g. call a use case.
    // Call displaySnackBar or pop() on success/failure.
  }
}

4. Interact with the FormBloc #

// Update a field value:
bloc.add(BlocxFormEventUpdateData<SignUpField>(
  field: SignUpField.email,
  value: 'user@example.com',
));

// Submit the form:
bloc.add(BlocxFormEventSubmit());

Migrating from 0.7.x #

Breaking: mixin renames #

All collection mixin names have had the redundant _bloc segment removed for a cleaner, consistent naming scheme. Update your with clauses and any direct imports:

Before (0.7.x) After (0.8.0)
BlocxInfiniteListBlocMixin BlocxCollectionInfiniteMixin
BlocxSelectableListBlocMixin BlocxCollectionSelectableMixin
BlocxRefreshableListBlocMixin BlocxCollectionRefreshableMixin
BlocxSearchableListBlocMixin BlocxCollectionSearchableMixin
BlocxDeletableListBlocMixin BlocxCollectionDeletableMixin
BlocxExpandableListBlocMixin BlocxCollectionExpandableMixin
BlocxHighlightableListBlocMixin BlocxCollectionHighlightableMixin
BlocxScrollableListBlocMixin BlocxCollectionScrollableMixin
BlocxListBlocSyncStreamMixin BlocxCollectionSyncStreamMixin

Form mixins follow the same blocx_form_* prefix pattern:

Before (0.7.x) After (0.8.0)
BlocxInfoFetcherFormMixin BlocxFormInfoFetcherMixin
BlocxSteppedFormMixin BlocxFormSteppedMixin

Breaking: model rename #

BaseFormEntity is now BlocxBaseFormEntity. Update all subclasses and type references.

Breaking: ScreenManagerCubit ownership #

ScreenManagerCubit is now owned internally by BaseBloc. Remove the screen parameter from your bloc constructors and call sites:

// Before
MyBloc({required ScreenManagerCubit screen}) : super(screen, MyStateInitial());

// After
MyBloc() : super(MyStateInitial());

pubspec constraint #

dependencies:
  blocx_core: ^0.8.0

Contributing #

Contributions are welcome. Please follow these guidelines:

  • Code style: Run dart format . before committing. All lints in analysis_options.yaml must pass (dart analyze).
  • Documentation: All public APIs must be documented with dartdoc comments.
  • Tests: Add or update tests for every new mixin, event, state, or validator. Run dart test to verify the full test suite passes.
  • Pull requests: Keep changes focused. One feature or fix per pull request.

License #

This project is licensed under the MIT License. See the LICENSE file at the repository root for details.

0
likes
0
points
424
downloads

Publisher

unverified uploader

Weekly Downloads

Composable BLoC mixins and use-cases for lists & forms (Dart-only core).

Repository (GitHub)
View/report issues

Topics

#bloc #architecture #lists #form

License

unknown (license)

Dependencies

bloc, bloc_concurrency, logger, meta, stream_transform, uuid

More

Packages that depend on blocx_core