flutter_blocx 1.2.0
flutter_blocx: ^1.2.0 copied to clipboard
Flutter UI widgets for paginated lists, infinite grids, slivers, reactive forms, and screen state management powered by blocx_core.
flutter_blocx
Zero-Boilerplate Flutter UI Widgets for Paginated Lists, Grids, Slivers, Reactive Forms & Screen Management
The official Flutter presentation layer for blocx_core.
Why flutter_blocx? • BlocX Ecosystem • Installation • Collections & Grids • Forms • Screen Manager • ← blocx_core
Why flutter_blocx? #
Most Flutter apps don't become difficult to maintain because of complex animations—they become difficult to maintain because every data-driven screen repeatedly wires the same UI plumbing:
- Attaching and disposing
ScrollControllers to trigger infinite pagination - Coordinating loading, empty, error, pull-to-refresh, and search states
- Wiring
BlocConsumers for item selection, row expansion, highlighting, and deletion spinners - Creating, hydrating, and disposing
TextEditingControllerandFocusNodemaps for every form - Displaying validation errors, async uniqueness spinners, and duplicate-submit guards
- Listening for snackbar, full-page error, and route-pop side effects
flutter_blocx eliminates that UI plumbing. Paired with blocx_core, you declare what your screen renders—and let the host state handle the controllers, listeners, pagination triggers, and side effects.
Before vs. After #
| Manual Flutter + BLoC Screen (~300+ lines) | With flutter_blocx + blocx_core (~35 lines) |
|---|---|
|
|
The BlocX Ecosystem: Better Together #
flutter_blocx is designed specifically to work hand-in-hand with blocx_core. Using both packages together gives you a cohesive, end-to-end architecture where domain logic stays 100% pure Dart and Flutter widgets remain declarative and concise:
| Layer | Package | Responsibilities |
|---|---|---|
| Domain & State (Pure Dart) | blocx_core (GitHub) |
BlocxBaseEntity, BlocxBaseUseCase, BlocxEventHub live CRUD sync, BlocxCollectionBloc (10 mixins), BlocxFormBloc (5 mixins + 35+ validators), ScreenManagerCubit |
| Presentation & UI (Flutter) | flutter_blocx (you are here) |
BlocxCollectionWidgetState, BlocxFormWidgetState, BlocxCollectionItem, InfiniteList / InfiniteGrid / AnimatedInfiniteList, textField / dropdown / checkbox, BlocxScreenManagerState, BlocxErrorWidget, BlocxSnackBar, ConfirmActionWidget |
Always install both packages together in your Flutter project so your BLoCs, UseCases, and Flutter screens share the same strongly typed contracts.
Feature Highlights #
- 📋 Collection Host & 6 Layout Engines (
list_widget.dart): Switch effortlessly betweenInfiniteList,AnimatedInfiniteList,SliverInfiniteList,AnimatedSliverInfiniteList,InfiniteGrid, andSliverInfiniteGridviaCollectionSettings. - 🃏 Smart Collection Items:
BlocxCollectionItem(stateless) andBlocxStatefulCollectionItem+BlocxCollectionItemState(stateful) with built-in helpers forisSelected,isExpanded,isHighlighted,isBeingRemoved, and modal delete confirmation. - 🔍 Built-in Search & Scroll-Aware FAB: Drop in
searchField()for debounced search and mix inHideOnScrollFabMixinfor a FloatingActionButton that hides on scroll-down and reappears on scroll-up. - 📝 Reactive Form Host & Controls (
form_widget.dart):BlocxFormWidgetStateautomatically managesTextEditingControllers andFocusNodes, hydrates edit-mode data, and providestextField()(with typeconverter, RTL auto-detection, and async uniqueness spinner),dropdown<T>(),checkbox(),BlocxFormRegisterButton, andBlocxFormButtonRow. - 🖥️ Unified Screen Management:
BlocxScreenManagerStateautomatically rendersBlocxSnackBars, full-screenBlocxErrorWidgets (with stack-trace expansion and retry callbacks), and route pops emitted by anyblocx_coreBLoC.
Installation #
Add both blocx_core and flutter_blocx to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
blocx_core: ^1.1.0
flutter_blocx: ^1.1.0
Or run:
flutter pub add blocx_core flutter_blocx
Barrel Imports #
// Full package barrel (Collection widgets, Form widgets, ScreenManager, SnackBar, ErrorWidget, ConfirmAction)
import 'package:flutter_blocx/flutter_blocx.dart';
// Or import modular barrels:
import 'package:flutter_blocx/list_widget.dart';
import 'package:flutter_blocx/form_widget.dart';
import 'package:flutter_blocx/blocx_collection_item_state.dart';
Collection Screens, Lists, Grids & Items #
1. Collection Screen Host (BlocxCollectionWidget & BlocxCollectionWidgetState) #
Extend BlocxCollectionWidget<Payload> and BlocxCollectionWidgetState<W, Entity, Payload> to build any list, grid, or sliver collection screen backed by a BlocxCollectionBloc from blocx_core:
import 'package:blocx_core/collection_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blocx/flutter_blocx.dart';
class ProductsScreen extends BlocxCollectionWidget<void> {
const ProductsScreen({super.key});
@override
State<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState
extends BlocxCollectionWidgetState<ProductsScreen, ProductEntity, void>
with HideOnScrollFabMixin<void> {
// 1. Required GETTER: instantiate your BlocxCollectionBloc
@override
BlocxCollectionBloc<ProductEntity, void> get generateBloc => ProductsBloc();
// 2. Wrap in a Scaffold with HideOnScrollFabMixin support
@override
bool get wrapInScaffold => true;
@override
Widget scaffoldWidget(BuildContext context, Widget body) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: NotificationListener<UserScrollNotification>(
onNotification: onScrollNotification,
child: body,
),
floatingActionButton: getFloatingActionButton(context),
);
}
@override
void onFabPressed(void data) {
// Open create product form or dispatch addToList(...)
}
// 3. Choose your layout engine (animatedList, list, grid, sliverList, animatedSliverList, sliverGrid)
@override
CollectionSettings get settings => CollectionSettings(
type: CollectionWidgetStateType.grid,
options: const InfiniteGridOptions(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
padding: EdgeInsets.all(12),
),
);
// 4. Optional top widget: built-in debounced searchField()
@override
Widget? topWidget(BuildContext context, BlocxCollectionState<ProductEntity> state) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: searchField(
options: const BlocxSearchFieldOptions(hintText: 'Search products...'),
),
);
}
// 5. Optional bottom widget: bulk selection action bar
@override
Widget? bottomWidget(BuildContext context, BlocxCollectionState<ProductEntity> state) {
if (state.selectedCount == 0) return null;
return Card(
margin: const EdgeInsets.all(12),
child: ListTile(
title: Text('${state.selectedCount} selected'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.deselect),
onPressed: () => deselectMultipleItems(state.selectedItems),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => deleteMultipleItems(state.selectedItems),
),
],
),
),
);
}
// 6. Required itemBuilder
@override
Widget itemBuilder(BuildContext context, ProductEntity item) {
return ProductCard(key: ValueKey(item.identifier), item: item);
}
}
Layout Types & Matching Options (CollectionSettings) #
CollectionWidgetStateType |
Matching options Class |
Description |
|---|---|---|
CollectionWidgetStateType.animatedList (default) |
AnimatedInfiniteListOptions |
Infinite list with animated insert/remove transitions |
CollectionWidgetStateType.list |
InfiniteListOptions |
Standard infinite ListView.separated |
CollectionWidgetStateType.grid |
InfiniteGridOptions(crossAxisCount: ...) |
Infinite GridView with pull-to-refresh |
CollectionWidgetStateType.sliverList |
SliverInfiniteListOptions |
Sliver infinite list (CustomScrollView) |
CollectionWidgetStateType.animatedSliverList |
AnimatedSliverInfiniteListOptions |
Animated sliver infinite list |
CollectionWidgetStateType.sliverGrid |
SliverInfiniteGridOptions(crossAxisCount: ...) |
Sliver infinite grid (CustomScrollView) |
2. Standalone Composable Views (BlocxCollectionView & BlocxFormView) #
Need to embed a collection list or a form inside a custom layout, tab view, dialog, or multi-pane desktop screen without subclassing BlocxCollectionWidgetState or BlocxFormWidgetState? Use the standalone composable views:
// Standalone collection list view
BlocxCollectionView<ProductEntity, void>(
bloc: productsBloc,
itemBuilder: (context, item) => ProductCard(item: item),
emptyBuilder: (context) => const Center(child: Text('No products available')),
errorBuilder: (context, error) => BlocxErrorWidget(
error: error,
onRetry: () => productsBloc.loadInitialPage(),
),
);
// Standalone form view
BlocxFormView<ProfileFormEntity, UserProfileEntity, ProfileFormField>(
bloc: profileBloc,
formBuilder: (context, state) => Column(children: [...]),
loadingBuilder: (context) => const Center(child: CircularProgressIndicator()),
);
3. DI-Friendly BLoC Construction #
BlocxCollectionWidget and BlocxFormWidget support three flexible dependency-injection patterns:
- Direct constructor injection: Pass
bloc: myBlocdirectly toBlocxCollectionWidget(bloc: myBloc)orBlocxFormWidget(bloc: myBloc). - Inherited Provider: Provide the BLoC typed as the base class, e.g.
BlocProvider<BlocxCollectionBloc<ProductEntity, void>>.value(...)orBlocProvider<BlocxFormBloc<ProfileForm, User, ProfileField>>.value(...). WhengenerateBlocis not overridden in your state subclass, it automatically resolvescontext.read<BlocxCollectionBloc<Entity, Payload>>()(orBlocxFormBloc<F, P, E>()). If provided under a concrete subtype, pass it directly via constructorMyWidget(bloc: myBloc)or overrideget generateBloc => myBloc. - Internal instantiation: Subclass and override
get generateBloc => MyBloc().
When a BLoC is injected via constructor or context, autoDisposeBloc / autoCloseBloc default to false so ancestor providers retain full lifecycle ownership.
4. Automatic Payload Reload (shouldReloadOnPayloadChange) #
Both BlocxCollectionWidgetState and BlocxFormWidgetState listen for incoming widget updates via didUpdateWidget. When widget.payload changes, reload() is automatically called to refresh the collection or re-initialize the form. To disable automatic reloads upon payload changes, override:
@override
bool get shouldReloadOnPayloadChange => false;
5. Collection Item Widgets (BlocxCollectionItem & BlocxStatefulCollectionItem) #
Stateless Item (BlocxCollectionItem<Entity, Payload>)
class ProductCard extends BlocxCollectionItem<ProductEntity, void> {
const ProductCard({super.key, required super.item});
@override
ConfirmActionOptions get confirmDeleteOptions => ConfirmActionOptions(
title: 'Delete ${item.name}?',
question: 'Are you sure you want to permanently remove "${item.name}"?',
);
@override
Widget buildContent(BuildContext context, ProductEntity item) {
final cs = colorScheme(context);
final selected = isSelected(context);
final removing = isBeingRemoved(context);
return Card(
color: selected ? cs.primaryContainer : null,
child: ListTile(
onTap: () => toggleSelection(context),
title: Text(item.name),
subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: removing
? const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(Icons.delete_outline, color: cs.error),
onPressed: removing ? null : () => removeItem(context),
),
),
);
}
}
Stateful Item (BlocxStatefulCollectionItem<Entity> & BlocxCollectionItemState<W, Entity, Payload>)
When an item needs local State (e.g., an AnimationController or inline TextEditingController), extend BlocxStatefulCollectionItem<T> and BlocxCollectionItemState<W, T, P>. Inside BlocxCollectionItemState, override Widget buildContent(T item) and use context-free getters/methods (isSelected, isExpanded, isBeingRemoved, toggleSelection(), toggleExpansion(), removeItem()).
Form Screens & Reactive Controls #
Form Screen Host (BlocxFormWidget & BlocxFormWidgetState) #
Extend BlocxFormWidget<Payload> and BlocxFormWidgetState<W, FormEntity, Payload, FieldEnum> to connect a BlocxFormBloc from blocx_core to Flutter form controls with automatic controller/focus management:
import 'package:blocx_core/form_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blocx/flutter_blocx.dart';
class ProfileFormScreen extends BlocxFormWidget<UserProfileEntity> {
const ProfileFormScreen({super.key, super.payload});
@override
State<ProfileFormScreen> createState() => _ProfileFormScreenState();
}
class _ProfileFormScreenState extends BlocxFormWidgetState<
ProfileFormScreen,
ProfileFormEntity,
UserProfileEntity,
ProfileFormField> {
// 1. Required GETTER: instantiate your BlocxFormBloc
@override
BlocxFormBloc<ProfileFormEntity, UserProfileEntity, ProfileFormField>
get generateBloc => ProfileFormBloc();
// 2. Field keys managed by the form
@override
List<ProfileFormField> get keys => ProfileFormField.values;
@override
bool get wrapInScaffold => true;
@override
Widget scaffoldWidget(BuildContext context, Widget body) {
return Scaffold(
appBar: AppBar(title: Text(isUpdate ? 'Edit Profile' : 'Create Profile')),
body: body,
);
}
// 3. Hydrate text controllers in edit mode or on live stream sync
@override
void applyInitialDataToForm(ProfileFormEntity formData) {
getTextEditingController(ProfileFormField.username).text = formData.username;
getTextEditingController(ProfileFormField.email).text = formData.email;
getTextEditingController(ProfileFormField.age).text =
formData.age > 0 ? formData.age.toString() : '';
}
// 4. React after successful submission
@override
void onFormSubmitted(
BlocxFormStateFormSubmitted<ProfileFormEntity, ProfileFormField> state,
) {
Navigator.of(context).maybePop(state.submittedData);
}
// 5. Build form UI with built-in reactive controls
@override
Widget formWidget(
BuildContext context,
BlocxFormState<ProfileFormEntity, ProfileFormField> state,
) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: formVerticalSpacing,
children: [
textField(
ProfileFormField.username,
type: TextFieldType.outlined,
labelText: 'Username',
prefix: const Icon(Icons.person_outline),
),
textField(
ProfileFormField.email,
type: TextFieldType.outlined,
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
),
textField(
ProfileFormField.age,
type: TextFieldType.outlined,
labelText: 'Age',
keyboardType: TextInputType.number,
converter: (value) => int.tryParse(value) ?? 0,
),
dropdown<String>(
ProfileFormField.role,
options: const BlocXDropdownOptions(labelText: 'Role'),
items: const [
DropdownMenuItem(value: 'admin', child: Text('Admin')),
DropdownMenuItem(value: 'member', child: Text('Member')),
],
),
checkbox(
key: ProfileFormField.acceptTerms,
isChecked: state.formData.acceptTerms,
options: BlocxCheckboxOptions(
isChecked: state.formData.acceptTerms,
label: 'I accept the terms and conditions',
),
),
BlocxFormButtonRow<ProfileFormEntity, UserProfileEntity, ProfileFormField>(
formState: state,
registerText: isUpdate ? 'Save Changes' : 'Create Profile',
registerSubmittingText: 'Saving...',
),
],
),
);
}
}
Built-in Form Controls #
| Host Helper / Widget | Key Features |
|---|---|
textField(...) / BlocxFormTextField |
TextFieldType.filled / outlined / underlined, converter for numeric/custom types, automatic RTL/LTR text direction, clear button, and automatic suffix spinner during async BlocxUniqueFieldValidatorMixin checks |
dropdown<T>(...) / BlocxFormDropdown |
Pre-wired DropdownButtonFormField<T> with automatic validation error display |
checkbox(...) / BlocxFormCheckbox |
Renders a CheckboxListTile when label/subtitle is provided, or a compact Checkbox otherwise |
submitButton(...) / BlocxFormRegisterButton |
Reacts to isSubmitting, isCheckingUniqueField, and isLoadingRequiredFields with an inline progress indicator |
formButtonRow(...) / BlocxFormButtonRow |
Primary BlocxFormRegisterButton paired with a secondary cancel/back button (Navigator.maybePop()) |
Form Validation Precedence
flutter_blocx supports two complementary validation mechanisms:
- BlocX Reactive Validation (Recommended): Configured on the bloc via
BlocxFormValidatorrules, async uniqueness checks (BlocxUniqueFieldValidatorMixin), and step validation. Errors are reactively streamed intobloc.state.errorsand rendered viaInputDecoration.errorText. - Flutter
validator:Hook: An optional standardFormFieldValidator<String>passed totextField(..., validator: ...).
Precedence Rules:
- When Flutter's
FormState.validate()runs, any non-null error string returned by Flutter'svalidatorhook takes visual precedence inFormFieldState. - When the Flutter validator returns
null(passes), any active bloc-level error for that field inbloc.state.errorsremains rendered on screen. - Guideline: Place domain validation rules and asynchronous checks in your
BlocxFormBlocusing pure-DartBlocxFieldValidatorclasses. Use Flutter'svalidator:hook for local UI formatting or when interoperating with third-party form wrappers.
Automatic Focus Management & requestFocusOnError
BlocxFormWidgetState manages FocusNode instances for all fields declared in keys:
- Calling
textField(key)ordropdown(key)automatically attaches the managed focus node fromgetFocusNode(key). You can also supply a customfocusNode:if needed. - Calling
requestFocusOnError(state)focuses the first field with an active error, traversing fields in the declaration order ofkeys(rather than arbitrary map iteration order).
Screen Management & Shared UX #
1. BlocxScreenManagerState<T> #
Both BlocxCollectionWidgetState and BlocxFormWidgetState extend BlocxScreenManagerState. You can also extend BlocxScreenManagerState<MyScreen> directly on any custom screen to get automatic handling for:
- Snackbars (
ScreenManagerCubitStateDisplaySnackbar$\rightarrow$BlocxSnackBar.show) - Full-Page Errors (
ScreenManagerCubitStateDisplayErrorPage$\rightarrow$errorWidget(context, state)) - Route Popping (
ScreenManagerCubitStatePop$\rightarrow$Navigator.maybePop())
2. BlocxErrorWidget #
Render Material 3 full-page error cards with collapsible stack traces, copy-to-clipboard, report, and retry actions:
@override
Widget errorWidget(
BuildContext context,
ScreenManagerCubitStateDisplayErrorPage state,
) {
return BlocxErrorWidget.fromState(
state,
onRetry: refreshData,
);
}
3. BlocxSnackBar #
Show themed floating snackbars (BlocxSnackbarType.info, warning, error):
BlocxSnackBar.show(
context,
title: 'Saved',
message: 'Your changes have been saved.',
type: BlocxSnackbarType.info,
);
4. ConfirmActionWidget #
Display a modal confirmation bottom sheet with optional type-to-confirm protection (requireTyping: true):
final confirmed = await ConfirmActionWidget.show(
context,
isScrollControlled: true,
options: ConfirmActionOptions(
title: 'Delete Account?',
question: 'This action is permanent. Type DELETE to confirm.',
confirmText: 'Delete Forever',
requireTyping: true,
deleteWord: 'DELETE',
),
);
5. Localization (BlocXLocalizations) #
All built-in labels (loadingText, emptyListText, tryAgain, cancel, delete, and validator messages) come from BlocXLocalizations in blocx_core. Override any string globally before runApp:
void main() {
BlocXLocalizations.localizations = MyAppLocalizations();
runApp(const MyApp());
}
Power Your Screens with blocx_core #
flutter_blocx widgets are powered by blocx_core (GitHub). Visit the blocx_core documentation to learn how to build:
BlocxCollectionBlocwith infinite scroll, search, filter, selection, deletion, and liveBlocxEventHubsynchronizationBlocxFormBlocwith 35+ built-in validators, async uniqueness checks, data prefetching, multi-step wizards, and live form synchronizationBlocxBaseUseCase&BlocxEventHubfor clean domain architecture and automatic cross-BLoC CRUD event broadcasting
👉 Explore blocx_core on pub.flutter-io.cn
Included AI Coding Skill #
This repository includes an AI agent skill at skills/flutter-blocx/SKILL.md (compatible with Claude Code, Antigravity, and Cursor) containing full widget rules, type-signature tables, blueprints, and progressive-disclosure reference guides for flutter_blocx.
Limitations #
- BLoC Instance Immutability: Dynamically mutating or replacing
widget.blocon an existing widget state is not supported.didUpdateWidgetassertswidget.bloc == oldWidget.bloc. To switch BLoCs, re-create the widget subtree or provide the bloc via an ancestorBlocProvider. - Inherited Provider Typing: When resolving BLoCs from
BuildContext(context.read), the provider must be typed as the base class (BlocProvider<BlocxCollectionBloc<Entity, Payload>>orBlocProvider<BlocxFormBloc<F, P, E>>). If provided under a concrete subtype, pass it directly via constructorMyWidget(bloc: myBloc)or overrideget generateBloc => myBloc. - Sliver Context Requirement: Sliver collection display types (
CollectionWidgetStateType.sliverList,sliverGrid,animatedSliverList,animatedSliverGrid) must be placed inside aCustomScrollViewor sliver viewport. - Form Key Ordering:
requestFocusOnErrorresolves error focus using the declaration order ofkeys. Ensure yourkeysgetter defines fields in the visual top-to-bottom layout order of your form.
Contributing & License #
Contributions, issues, and feature requests are welcome at the issue tracker.
Released under the MIT License.