xwidgets_pack 1.2.0
xwidgets_pack: ^1.2.0 copied to clipboard
The library focuses on lightweight, well-documented components that are easy to style and compose.
XWidgets Pack #
Reusable Flutter widgets for building consistent interfaces with less boilerplate.
Table of contents # |
[XWidgets example] |
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.