xwidgets_pack 1.3.0
xwidgets_pack: ^1.3.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.
Getting started · Look presets · Widget catalog · UI showcase · Examples
Table of contents #
- UI showcase
- Why XWidgets?
- Getting started
- Look presets
- Widget catalog
- XAsyncView
- XDebouncedSearchField
- XResponsiveLayout
- XScrollView
- XCollectionView
- XButton states
- XDialog and XBottomSheet
- XScreen
- Other examples
- Complete examples
- License
UI showcase #
Screen recordings from the example app — each look preset applied to the same widget set.
Standard |
Material |
iOS |
Glassmorphism |
Neumorphism |
Retro |
Neo-Brutalism |
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.
- Optional visual look presets (
standard,material,ios,glass,neumorphism,retro,neoBrutalism) on visual widgets.
Getting started #
Add the package:
dependencies:
xwidgets_pack: ^1.3.0
Install dependencies and import the public library:
flutter pub get
import 'package:xwidgets_pack/xwidgets.dart';
Look presets #
Visual widgets accept an optional look: parameter backed by the public
XLook enum. The default is XLook.standard, which preserves the
existing package look. Upgrading does not change current UIs unless you opt in.
import 'package:xwidgets_pack/xwidgets.dart';
// Unchanged — same as before
XButton(label: 'Save', onPressed: onSave);
// Opt into a preset
XButton(label: 'Save', onPressed: onSave, look: XLook.ios);
XCard(look: XLook.glass, child: content);
XTextField(hintText: 'Email', look: XLook.material);
XDialog.alert(context, title: 'Hi', message: 'Hello', look: XLook.retro);
Available looks #
XLook |
Description |
|---|---|
standard |
Existing package defaults (backward-compatible) |
material |
Material 3-inspired shapes, tonal surfaces, and colors |
ios |
Large radius, light borders, low elevation |
glass |
Translucent fill with blur (glassmorphism) |
neumorphism |
Soft extruded surfaces with dual shadows |
retro |
Muted vintage palette with firm borders |
neoBrutalism |
Thick borders, hard offset shadows, high contrast |
Supported widgets #
| Widget | look: support |
|---|---|
XButton |
Constructor parameter |
XCard |
Constructor parameter |
XTextField |
Constructor parameter |
XAppBar |
Constructor parameter |
XText |
Constructor parameter |
XSingleDashedLine, XDoubleDashedLine |
Constructor parameter |
XShimmerChild |
Constructor parameter |
XDialog.alert, XDialog.confirm, XDialog.loading |
Method parameter |
XBottomSheet.show, XBottomSheet.actions |
Method parameter |
XSnackbar.info, success, error, warning, custom |
Method parameter |
Layout and behavior widgets such as XScrollView, XAsyncView,
XSpacer, and XResponsiveLayout are intentionally unchanged.
Override rules #
Look presets only fill default visual values. Explicit props still win:
// look sets the default radius, but radius: 24 overrides it
XButton(
label: 'Save',
onPressed: onSave,
look: XLook.ios,
radius: 24,
);
// style: overrides the look button colors
XButton(
label: 'Save',
onPressed: onSave,
look: XLook.retro,
style: XButtonStyle(background: Colors.purple),
);
The same rule applies to XTextField.style, XCard.background, XAppBar.backgroundColor,
and other explicit styling parameters.
Dynamic look selection #
Because look is a normal parameter, it can come from app state or config:
final look = isIosPlatform ? XLook.ios : XLook.standard;
XButton(
label: 'Continue',
onPressed: onContinue,
look: look,
);
Per-widget examples #
Button and card
XButton(label: 'Primary', onPressed: onTap, look: XLook.material);
XCard(
look: XLook.neumorphism,
padding: const EdgeInsets.all(16),
child: const Text('Soft card'),
);
Text field and app bar
XAppBar(title: 'Settings', look: XLook.ios);
XTextField(
look: XLook.material,
labelOnLine: 'Email',
hintText: 'you@example.com',
);
Overlays
XSnackbar.success('Saved', look: XLook.glass);
await XDialog.confirm(
context,
title: 'Delete item?',
message: 'This cannot be undone.',
look: XLook.neoBrutalism,
isDestructive: true,
);
await XBottomSheet.actions<String>(
context,
look: XLook.ios,
title: 'Share',
actions: const [
XBottomSheetAction(label: 'Copy link', value: 'copy'),
XBottomSheetAction(label: 'Send', value: 'send'),
],
);
Text, dividers, and shimmer
XText('Terms of service', look: XLook.ios, isUseUnderline: true);
const XSingleDashedLine(look: XLook.retro);
const XDoubleDashedLine(look: XLook.neoBrutalism);
const XShimmerChild(height: 48, look: XLook.glass);
Tips #
- Glass looks best on colorful backgrounds — place
XCard(look: XLook.glass)over gradients or images. - Neumorphism works best on a matching flat background (for example
Color(0xFFE0E5EC)). XLook.standardandmaterialare different —standardkeeps the original package defaults;materialapplies a stricter Material 3-inspired preset.
Runnable showcase #
Open the example app and choose a look from the home screen to compare presets on dedicated pages or open the full widget showcase for Standard and Material.
See example/lib/look_picker_page.dart and example/lib/look_themed_example.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; optional look: |
XDialog, XBottomSheet |
Typed dialogs, confirmations, loading, and action sheets; optional look: |
XScreen |
Scaffold, safe area, content width, loading, and error overlays |
XTextField |
Normal, file, dropdown, date, and time fields with validation; optional look: |
XAppBar |
App bar wrapper with common title, leading, and action options; optional look: |
XText |
Text with icon, underline, and tap support; optional look: |
XCard |
Consistent card layout and styling; optional look: |
XSnackbar |
Success, warning, error, and custom snackbar helpers; optional look: |
XShimmer |
Loading placeholders; XShimmerChild supports optional look: |
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.
Combine button states with look presets when needed:
XButton(
state: saveState,
label: 'Save',
look: XLook.ios,
onPressed: save,
);
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.
Pass look: on alert, confirm, loading, show, and actions helpers to style
the chrome. See Look presets.
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 #
Look presets
// Default — existing package look
XButton(label: 'Save', onPressed: onSave);
// Preset looks
XButton(label: 'Save', onPressed: onSave, look: XLook.ios);
XCard(look: XLook.glass, child: const Text('Frosted card'));
XText('Headline', look: XLook.neoBrutalism);
XSnackbar.success('Saved', look: XLook.material);
await XDialog.alert(
context,
title: 'Notice',
message: 'Hello from a retro dialog.',
look: XLook.retro,
);
See Look presets for the full reference, example/lib/look_picker_page.dart for the look selection home screen, and example/lib/look_themed_example.dart for dedicated themed pages.
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/look_picker_page.dart for the look selection home screen and example/lib/look_themed_example.dart for dedicated iOS, Glass, Neumorphism, Retro, and Neo-Brutalism pages.
See example/lib/other_widgets_example.dart
for an executable page combining XAsyncView, XDebouncedSearchField,
XResponsiveLayout, XCollectionView, stateful XButton, XDialog,
XBottomSheet, and XScreen.
Run the example app from the package root:
cd example
flutter pub get
flutter run
License #
Released under the MIT License.