XWidgets Pack
Reusable Flutter widgets for building consistent interfaces with less boilerplate.
Table of contents |
|
Why XWidgets?
XWidgets provides practical building blocks on top of Flutter's native widgets. Each component remains configurable, composable, and independent from a specific state-management package.
- Reusable buttons, cards, text, and app bars.
- Form validation and file, date, and dropdown fields.
- Initial loading and paginated lists.
- Empty, error, retry, refresh, and loading states.
- Shimmer, snackbar, spacing, and decorative helpers.
Getting started
Add the package:
dependencies:
xwidgets_pack: ^1.2.0
Install dependencies and import the public library:
flutter pub get
import 'package:xwidgets_pack/xwidgets.dart';
Widget catalog
| Widget | Purpose |
|---|---|
XAsyncView<T> |
Standard initial, loading, data, empty, error, and retry UI |
XDebouncedSearchField |
Search input with debounce, clear, submit, and loading |
XResponsiveLayout |
Mobile, tablet, and desktop layout switching |
XScrollView<T> |
Vertical/horizontal lists, refresh, pagination, retry, and item interaction |
XCollectionView<T> |
List or grid using the same paginated data API |
XButton |
Configurable button with idle, loading, success, and error states |
XDialog, XBottomSheet |
Typed dialogs, confirmations, loading, and action sheets |
XScreen |
Scaffold, safe area, content width, loading, and error overlays |
XTextField |
Normal, file, dropdown, date, and time fields with validation |
XAppBar |
App bar wrapper with common title, leading, and action options |
XText |
Text with icon, underline, and tap support |
XCard |
Consistent card layout and styling |
XSnackbar |
Success, warning, error, and custom snackbar helpers |
XShimmer |
Loading placeholders |
XSpacer, XHeight, XWidth |
Layout spacing shortcuts |
| Dashed lines and strikethrough text | Decorative UI helpers |
XAsyncView
Use XAsyncState<T> in any state-management container and let the view choose
the correct presentation. Fetching remains outside the widget.
XAsyncState<String> userState = const XAsyncState.initial();
Future<void> loadUser() async {
setState(() => userState = const XAsyncState.loading());
try {
final name = await repository.getUserName();
setState(() {
userState = name.isEmpty
? const XAsyncState.empty()
: XAsyncState.data(name);
});
} catch (error, stackTrace) {
setState(() => userState = XAsyncState.error(error, stackTrace));
}
}
XAsyncView<String>(
state: userState,
onRetry: loadUser,
loadingBuilder: (_) => const UserSkeleton(),
emptyBuilder: (_) => const Text('No user found'),
errorBuilder: (_, error, retry) => ErrorPanel(
message: error.toString(),
onRetry: retry,
),
dataBuilder: (_, name) => Text('Hello, $name'),
);
Set showPreviousDataWhileLoading: true and create
XAsyncState.loading(previousData: oldData) to keep existing content visible
during a background reload.
XDebouncedSearchField
The callback runs only after typing stops for the configured duration. Asynchronous loading and stale-request indicators are handled internally; search results remain in your own state.
XDebouncedSearchField(
debounceDuration: const Duration(milliseconds: 400),
minimumQueryLength: 2,
decoration: const InputDecoration(
hintText: 'Search products',
prefixIcon: Icon(Icons.search),
),
onSearch: (query) async {
final products = await repository.searchProducts(query);
setState(() => searchResults = products);
},
onClear: () => setState(() => searchResults = []),
onError: (error, stackTrace) {
debugPrint('Search failed: $error');
},
);
Use an external TextEditingController or FocusNode when another state
object needs to control the field.
XResponsiveLayout
Layouts are selected from available parent width, not only physical screen width, so the widget also works inside panels and split-screen interfaces.
XResponsiveLayout(
breakpoints: const XBreakpoints(
tablet: 600,
desktop: 1024,
),
mobile: (_, constraints) => const MobileDashboard(),
tablet: (_, constraints) => const TabletDashboard(),
desktop: (_, constraints) => const DesktopDashboard(),
);
You can inspect the current category outside the widget:
final size = XResponsiveLayout.sizeOf(context);
final isDesktop = size == XResponsiveSize.desktop;
XScrollView
XScrollView<T> owns its loading presentation while data fetching stays in
your callback. The same API works with setState, Provider, Riverpod, BLoC,
GetX, MobX, or a custom controller.
Future<XScrollPage<Product>> fetchProducts(XScrollRequest request) async {
final response = await repository.getProducts(
offset: request.offset,
limit: request.limit,
);
return XScrollPage(
items: response.products,
hasMore: response.hasNextPage,
);
}
XScrollView<Product>(
pageSize: 20,
onInit: fetchProducts,
onLoadMore: fetchProducts,
onItemsChanged: (items) {
// Optional: synchronize the accumulated list to any state manager.
},
onItemTap: (product, index) {
Navigator.pushNamed(context, '/product', arguments: product);
},
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, product, index) {
return ListTile(
title: Text(product.name),
subtitle: Text(product.priceLabel),
);
},
);
The request contains:
| Property | Meaning |
|---|---|
page |
One-based page number |
offset |
Number of items already loaded |
limit |
Requested item count, configured through pageSize |
isRefresh |
true when triggered by pull-to-refresh |
XScrollPage.hasMore is optional. When it is omitted, the widget considers a
short page (items.length < limit) to be the last page.
State and layout customization
Use the supplied builders and scroll properties to match your application:
XScrollView<Message>(
pageSize: 15,
paginationThreshold: 300,
onInit: loadMessages,
onLoadMore: loadMessages,
loadingBuilder: (_) => const MessageListSkeleton(),
emptyBuilder: (_) => const EmptyInbox(),
errorBuilder: (_, error, retry) => ErrorPanel(
message: error.toString(),
onRetry: retry,
),
paginationLoadingBuilder: (_) => const LinearProgressIndicator(),
refreshIndicatorBuilder: (_, progress, isRefreshing) {
return CircularProgressIndicator(
value: isRefreshing ? null : progress,
);
},
padding: const EdgeInsets.all(16),
physics: const BouncingScrollPhysics(),
itemBuilder: (_, message, __) => MessageTile(message: message),
);
Set autoLoad: false to display initialItems without calling onInit
automatically. Set enableRefresh: false to disable pull-to-refresh.
Horizontal list
Set scrollDirection to use the same loading, pagination, item tap, retry, and
refresh behavior horizontally:
SizedBox(
height: 160,
child: XScrollView<Product>(
scrollDirection: Axis.horizontal,
pageSize: 10,
onInit: fetchProducts,
onLoadMore: fetchProducts,
separatorBuilder: (_, _) => const VerticalDivider(width: 12),
itemBuilder: (_, product, __) {
return SizedBox(
width: 140,
child: ProductCard(product: product),
);
},
),
);
Pull from the leading edge to refresh a horizontal list. Use
refreshTriggerExtent to configure the required drag distance and
refreshIndicatorBuilder to replace the indicator in either direction.
Without a custom builder, vertical lists use Flutter's native
RefreshIndicator and horizontal lists use the built-in XScrollView
indicator.
Custom loading indicators
Initial, pagination, and pull-to-refresh loading can be styled independently:
XScrollView<Product>(
onInit: fetchProducts,
onLoadMore: fetchProducts,
loadingBuilder: (_) => const ProductListSkeleton(),
paginationLoadingBuilder: (_) => const Padding(
padding: EdgeInsets.all(16),
child: Text('Loading more...'),
),
refreshIndicatorBuilder: (_, progress, isRefreshing) {
return MyRefreshIndicator(
progress: progress,
isRefreshing: isRefreshing,
);
},
itemBuilder: (_, product, __) => ProductTile(product: product),
);
For a vertical list, paginationLoadingBuilder appears at the bottom. For a
horizontal list, the same builder appears at the right/end side. The
progress value passed to refreshIndicatorBuilder ranges from 0.0 to
1.0; isRefreshing becomes true while onInit is fetching refreshed
data.
XCollectionView
XCollectionView exposes list and grid constructors while reusing the
XScrollRequest and XScrollPage<T> contract from XScrollView.
Paginated list
XCollectionView<String>.list(
pageSize: 20,
onInit: fetchNames,
onLoadMore: fetchNames,
separatorBuilder: (_, _) => const Divider(height: 1),
emptyBuilder: (_) => const Center(child: Text('No names')),
itemBuilder: (_, name, _) => ListTile(title: Text(name)),
);
Paginated grid
XCollectionView<String>.grid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.2,
),
pageSize: 20,
onInit: fetchProducts,
onLoadMore: fetchProducts,
emptyBuilder: (_) => const Center(child: Text('No products')),
itemBuilder: (_, product, _) => ProductCard(product: product),
);
Both constructors support pull-to-refresh, custom loading/error builders,
horizontal or vertical scrolling, item taps, and onItemsChanged.
XButton states
Button state is external and therefore maps directly to BLoC states, Riverpod
providers, ChangeNotifier, GetX controllers, or setState.
XButtonState saveState = XButtonState.idle;
Future<void> save() async {
setState(() => saveState = XButtonState.loading);
try {
await repository.save();
setState(() => saveState = XButtonState.success);
} catch (_) {
setState(() => saveState = XButtonState.error);
}
}
XButton(
state: saveState,
label: 'Save',
loadingLabel: 'Saving...',
successLabel: 'Saved',
errorLabel: 'Retry',
width: double.infinity,
onPressed: save,
);
Use child, loadingChild, successChild, or errorChild when each state
requires fully custom content. Existing isLoading and isLoadingInside
parameters remain supported for backward compatibility.
XDialog and XBottomSheet
Confirmation and alert
final confirmed = await XDialog.confirm(
context,
title: 'Delete item?',
message: 'This action cannot be undone.',
confirmLabel: 'Delete',
isDestructive: true,
);
if (confirmed && context.mounted) {
await repository.delete();
await XDialog.alert(
context,
title: 'Deleted',
message: 'The item was removed.',
);
}
Loading dialog
XDialog.loading<void>(
context,
message: 'Uploading...',
);
await repository.upload();
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
Typed action sheet
final source = await XBottomSheet.actions<String>(
context,
title: 'Select image source',
actions: const [
XBottomSheetAction(
label: 'Camera',
value: 'camera',
icon: Icon(Icons.camera_alt),
),
XBottomSheetAction(
label: 'Gallery',
value: 'gallery',
icon: Icon(Icons.photo),
),
],
);
For custom content, use XDialog.show<T> or XBottomSheet.show<T>. Both
return the typed value passed to Navigator.pop.
XScreen
XScreen combines common page behavior while preserving native Scaffold
slots.
XScreen(
appBar: AppBar(title: const Text('Profile')),
maxContentWidth: 900,
padding: const EdgeInsets.all(16),
isLoading: isSaving,
error: pageError,
onRetry: loadProfile,
loadingBuilder: (_) => const Center(
child: CircularProgressIndicator(),
),
errorBuilder: (_, error, retry) => ErrorPanel(
message: error.toString(),
onRetry: retry,
),
body: const ProfileForm(),
);
Enabled by default:
SafeAreaaround body content.- Keyboard dismissal when the background is tapped.
- Blocking loading overlay.
- Full-page error overlay with optional retry.
- Centered content constraint through
maxContentWidth.
Other examples
XButton
XButton(
label: 'Submit',
isLoading: isSubmitting,
isLoadingInside: true,
onPressed: submit,
style: XButtonStyle(
loadingColor: Colors.white,
loadingStrokeWidth: 2.5,
),
);
XTextField
XTextField(
labelOnLine: 'Email',
hintText: 'your@email.com',
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
textInputAction: TextInputAction.done,
validator: (value) => value == null || value.isEmpty ? 'Required' : null,
);
Dropdown, file, date, and time variants use the same widget:
XTextField(
label: 'Region',
fieldType: XTextFieldType.dropdown,
dropdownOptions: XTextFieldDropdownOptions(
items: const ['Sumatra', 'Java', 'Kalimantan'],
itemAsString: (item) => item,
),
onDropdownChanged: (value) {},
);
XAppBar
Scaffold(
appBar: XAppBar(
title: 'Dashboard',
actions: [
IconButton(
onPressed: openSearch,
icon: const Icon(Icons.search),
),
],
),
);
XText
XText(
'Account information',
icon: const Icon(Icons.info_outline, size: 18),
isExpand: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
onTap: openAccount,
);
XCard
XCard(
padding: const EdgeInsets.all(16),
radius: 12,
enableRipple: true,
onTap: openDetails,
child: const Text('Tap to open details'),
);
XSnackbar
Attach the navigator key once:
MaterialApp(
navigatorKey: XSnackbar.navigatorKey,
home: const App(),
);
Then show typed messages from anywhere:
XSnackbar.success('Data saved');
XSnackbar.warning(
'Connection is unstable',
position: XSnackbarPosition.top,
);
XSnackbar.error('Unable to save data', title: 'Error');
XShimmer
XShimmer(
isLoading: isLoading,
shimmerChild: const Column(
children: [
XShimmerChild(height: 80),
SizedBox(height: 12),
XShimmerChild(height: 16, width: 180),
],
),
child: ProductDetails(product: product),
);
XSpacer, XHeight, and XWidth
Column(
children: [
const Text('First'),
const XSpacer(height: 16),
const Text('Second'),
const XHeight(8),
],
);
Row(
children: [
const Icon(Icons.star),
const XWidth(8),
const Text('Favorite'),
],
);
Decorative widgets
const Column(
children: [
XSingleDashedLine(),
XDoubleDashedLine(),
XDiagonalStrikethroughText(
'Rp 150.000',
diagonalType: XDiagonalStrikethroughType.bottomTop,
lineColor: Colors.red,
),
],
);
Complete examples
See example/lib/example_xwidgets.dart for
the original widget showcase, including paginated XScrollView usage.
See example/lib/other_widgets_example.dart
for an executable page combining XAsyncView, XDebouncedSearchField,
XResponsiveLayout, XCollectionView, stateful XButton, XDialog,
XBottomSheet, and XScreen.
License
Released under the MIT License.
Libraries
- extensions/text_extension
- models/x_snackbar_config
- utils/x_form_validators
- utils/x_textfield_options
- utils/x_textfield_style
- widgets/shimmer/x_shimmer
- widgets/shimmer/x_shimmer_child
- widgets/shimmer/x_shimmer_effect
- widgets/x_app_bar
- widgets/x_async_view
- widgets/x_bottom_sheet
- widgets/x_card
- widgets/x_collection_view
- widgets/x_debounced_search_field
- widgets/x_diagonal_strikethrough_text
- widgets/x_dialog
- widgets/x_double_dashed_line
- widgets/x_responsive_layout
- widgets/x_screen
- widgets/x_scrollview
- widgets/x_single_dashed_line
- widgets/x_snackbar
- widgets/x_spacer
- widgets/x_text
- widgets/x_text_field
- xwidgets
- XWidgets