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.