exxen 0.1.0
exxen: ^0.1.0 copied to clipboard
Exxen is a Flutter package that offers extensions to make your code cleaner, reusable, and easier to maintain. It is compatible with WebAssembly (WASM).
Exxen is a Flutter package that offers extensions to make your code cleaner, reusable, and easier to maintain. It is compatible with WebAssembly (WASM).
dependencies:
exxen: ^0.1.0
import 'package:exxen/exxen.dart';
Every extension is named with the Exxension suffix and lives in
lib/src/exxention/, so a file in that folder always holds extensions and
nothing else. The two helpers that are classes rather than extensions,
LayoutSnapper and LayoutSnapperButton, live in lib/src/tools/. One
import gives you all of them.
| Exxension | On | What it shortens |
|---|---|---|
ContextExxension |
BuildContext |
theme, media query, breakpoints, navigation, dialogs |
WidgetExxension |
Widget |
every wrapper widget, without the nesting |
NumExxension |
num |
spacing, insets, radius, duration |
ListExxension |
List<Widget> |
columns, rows, grids, slivers, gaps |
StringPathExxension |
String |
asset paths for 43 file types |
StringExxension |
String |
case, validation, toText |
StringValidationExxension |
String |
email, url, phone, password |
StringTransformExxension |
String |
slug, initials, truncate, mask, Turkish case |
StringParseExxension |
String |
int, double, DateTime, Color |
ColorExxension |
Color |
darken, lighten, blend, hex, swatch |
DateTimeExxension |
DateTime |
bounds, comparisons, formatting, timeAgo |
DurationExxension |
Duration |
delay, HH:mm:ss, fromNow, ago |
FutureExxension |
Future<T> |
orNull, orElse, timeoutOrNull |
BoolExxension |
bool |
toInt, yesNo, pick, ifTrue |
IterableExxension |
Iterable<T> |
chunk, distinct, sum, average |
MapExxension |
Map<K, V> |
getOrElse, filters, inverted |
TextStyleExxension |
TextStyle? |
bold, size, withColor, chained |
ScrollControllerExxension |
ScrollController |
top, bottom, progress, infinite scroll |
SliverExxension |
Widget |
sliver wrappers for a CustomScrollView |
ValidatorExxension |
String? |
form field error messages |
LayoutExxension |
BuildContext |
dump the widget tree while debugging |
Every nullable type has its own extension too, so String?, bool?,
Iterable<T>? and Map<K, V>? all get isNullOrEmpty and an orEmpty fallback.
Context Exxension #
You can use very easy context elements,
// context.theme is the same as Theme.of(context)
// context.colorScheme is the same as Theme.of(context).colorScheme
backgroundColor: context.colorScheme.inversePrimary
// context.textTheme is the same as Theme.of(context).textTheme
style: context.textTheme.headlineMedium
Theme, screen and keyboard,
// context.isDarkMode is the same as Theme.of(context).brightness == Brightness.dark
if (context.isDarkMode) ...
// context.isMobile / isTablet / isDesktop read MediaQuery.of(context).size.width
if (context.isMobile) ...
// context.widthOf(0.5) is half of the screen width
width: context.widthOf(0.5)
// context.isKeyboardOpen is the same as MediaQuery.of(context).viewInsets.bottom > 0
if (context.isKeyboardOpen) context.hideKeyboard();
Navigation, snack bars, dialogs and sheets,
// context.push(page) is the same as
// Navigator.of(context).push(MaterialPageRoute(builder: (_) => page))
context.push(const DetailPage());
context.pushNamed('/detail');
context.pop();
context.maybePop();
// context.showSnackBar('...') is the same as
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('...')))
context.showSnackBar('Saved');
// context.openDialog(dialog) is the same as
// showDialog(context: context, builder: (_) => dialog)
context.openDialog(const AlertDialog(title: Text('Are you sure?')));
context.openBottomSheet(const FilterSheet(), isScrollControlled: true);
Widget Exxension #
You can wrap any widget without nesting it by hand,
// .padding() is the same as Padding(padding: ..., child: widget)
Text('Exxen').padding(16.all)
Text('Exxen').paddingAll(16)
Text('Exxen').paddingSymmetric(horizontal: 16, vertical: 8)
// .onTap() is the same as InkWell(onTap: ..., child: widget)
Icon(Icons.add).onTap(() {})
// .expanded() is the same as Expanded(flex: 2, child: widget)
Text('Exxen').expanded(flex: 2)
// .decorated() is the same as Container(decoration: BoxDecoration(...), child: widget)
Text('Exxen').decorated(
color: context.colorScheme.surface,
borderRadius: 12.radius,
boxShadow: const [BoxShadow(blurRadius: 8)],
)
// Chains read top to bottom, in the order the wrappers apply
Image.asset('logo'.pngPath('assets'))
.clipRRect(borderRadius: 12.radius)
.paddingAll(8)
.card(elevation: 2)
.center()
safeArea, card, material, constrained, aspectRatio, positioned,
ignorePointer, absorbPointer, repaintBoundary, tooltip, onLongPress,
onDoubleTap, scrollable, refreshable, sliver, animatedOpacity,
rotatedBox, intrinsicHeight, intrinsicWidth and circle all work the same way.
Num Exxension #
You can build spacing, insets, radius and duration from a plain number,
// 24.height is the same as SizedBox(height: 24)
// 24.width is the same as SizedBox(width: 24)
// 24.box is the same as SizedBox(width: 24, height: 24)
Column(children: [Text('Exxen'), 24.height, Text('Package')])
// 16.all is the same as EdgeInsets.all(16)
// 8.horizontal is the same as EdgeInsets.symmetric(horizontal: 8)
// 8.top is the same as EdgeInsets.only(top: 8)
padding: 16.all
// 12.radius is the same as BorderRadius.circular(12)
// 12.topRadius is the same as BorderRadius.vertical(top: Radius.circular(12))
borderRadius: 12.radius
// 300.ms is the same as Duration(milliseconds: 300)
// 2.seconds is the same as Duration(seconds: 2)
duration: 300.ms
// 2.spacer is the same as Spacer(flex: 2)
Row(children: [Text('left'), 1.spacer, Text('right')])
3.14159.toPrecision() // 3.14
20.percentOf(200) // 40
5.isBetween(1, 10) // true
7.padded() // 07
List Exxension #
You can turn a list of widgets into a layout,
// .toColumn() is the same as Column(children: [...])
// .toRow() / .toStack() / .toWrap() / .toListView() / .toGridView()
// .toPageView() / .toIndexedStack() / .toSliverList() / .toFlex() work the same way
[Text('Exxen'), Text('Package')].toColumn(
mainAxisAlignment: MainAxisAlignment.center,
)
// .gap(8) inserts a SizedBox between every item
[Text('Exxen'), Text('Package')].gap(8).toColumn()
// .separatedBy() inserts any widget between every item
[Text('Exxen'), Text('Package')].separatedBy(const Divider()).toColumn()
String Exxension #
You can build asset paths,
// 'img'.pngPath('assets/images') is the same as 'assets/images/img.png'
// jpgPath, svgPath, jsonPath, ttfPath, mp4Path and 38 more are available
Image.asset('img'.pngPath('assets/images'))
You can format, validate and parse text,
'exxen'.capitalize // Exxen
'flutter package'.toTitleCase // Flutter Package
'Flutter Package'.toSlug // flutter-package
'Oğuzhan Arslantaş'.initials() // OA
'Exxension'.truncate(3) // Exx...
'4242424242424242'.mask() // ************4242
'name@mail.com'.isEmail // true
'pub.dev/packages/exxen'.isUrl // true
'+90 555 123 45 67'.isPhone // true
r'Exxen.2026'.isStrongPassword // true
'42'.toIntOrNull // 42
'2026-09-03'.toDateTimeOrNull // DateTime(2026, 9, 3)
'#FF5722'.toColorOrNull // Color(0xFFFF5722)
// Dart uppercases the Turkish i as I, these two keep the dot where it belongs
'istanbul'.toTurkishUpperCase // İSTANBUL
'ILIK'.toTurkishLowerCase // ılık
// String? helpers
value.isNullOrEmpty
value.orEmpty('fallback')
// .toText() is the same as Text('...')
'Exxen'.toText(style: context.textTheme.headlineMedium)
Color Exxension #
context.colorScheme.primary.darken() // one shade darker
context.colorScheme.primary.lighten(0.2) // two shades lighter
Colors.black.blend(Colors.white, 0.25) // a quarter of the way to white
Colors.blue.fade(0.5) // half transparent
Colors.blue.isDark // true
Colors.blue.contrastingColor // Colors.white
Colors.blue.toHex() // #2196F3
Colors.blue.toMaterialColor // a full 50..900 swatch
Date Time Exxension #
date.startOfDay / endOfDay / startOfWeek / endOfWeek / startOfMonth / endOfMonth
date.isToday / isYesterday / isTomorrow / isPast / isFuture / isWeekend
date.isSameDay(other)
date.isBetween(start, end)
date.daysUntil(other)
birthday.ageInYears
date.toDateString() // 03.09.2026
date.toIsoDateString() // 2026-09-03
date.toTimeString() // 14:30
date.timeAgo // 5m ago
Duration and Future Exxension #
const Duration(hours: 1, minutes: 2, seconds: 3).toHms // 01:02:03
const Duration(minutes: 2, seconds: 3).toMs // 02:03
1.seconds.fromNow // DateTime
await 300.ms.delay();
await api.fetchUser().orNull; // null instead of throwing
await api.fetchUser().orElse(User.guest()); // a fallback instead of throwing
await api.fetchUser().timeoutOrNull(5.seconds); // null instead of hanging
Text Style Exxension #
It is declared on a nullable TextStyle, so it chains straight off a text theme
and always hands back a real style,
Text(
'Exxen',
style: context.textTheme.titleLarge.bold.size(24).withColor(Colors.blue),
)
// semiBold, medium, regular, light, italic, underline, lineThrough,
// weight, letterSpaced, lineHeight, family and fade work the same way
Iterable, Map and Bool Exxension #
The iterable members are named so that they do not clash with
package:collection, which many projects already import.
[1, 2, 3, 4, 5].chunked(2) // [[1, 2], [3, 4], [5]]
users.distinctBy((user) => user.email)
orders.sumBy((order) => order.total)
orders.averageBy((order) => order.total)
orders.countWhere((order) => order.isPaid)
orders.reject((order) => order.isPaid)
items.randomOrNull()
{'a': 1}.getOrElse('z', 0) // 0
{'a': 1, 'b': 2}.inverted // {1: 'a', 2: 'b'}
{'a': 1, 'b': null}.withoutNulls // {'a': 1}
isActive.toInt // 1
isActive.yesNo // Yes
isActive.pick('on', 'off')
maybeActive.orFalse // bool? -> bool
Scroll Exxension #
Every member guards on hasClients, so it is safe to read one before the
controller is attached,
final controller = ScrollController();
controller.isAtTop / isAtBottom
controller.progress // 0.0 at the top, 1.0 at the bottom
controller.isScrollingDown // true while the user drags the content up
await controller.scrollToTop();
await controller.scrollToBottom(duration: 500.ms);
await controller.scrollTo(320); // clamped into the scrollable range
controller.jumpToBottom();
// Infinite scrolling in one line. It hands back the listener it added,
// so you can remove it in dispose().
late final VoidCallback listener;
listener = controller.onEndReached(loadNextPage, threshold: 300);
@override
void dispose() {
controller.removeListener(listener);
controller.dispose();
super.dispose();
}
Sliver Exxension #
<Widget>[
const Text('Header').sliver().sliverPadding(16.all),
items.map(ItemTile.new).toList().toSliverList().sliverSafeArea(),
photos.toSliverGrid(crossAxisCount: 3),
const EmptyState().sliverFillRemaining(hasScrollBody: false),
].toCustomScrollView()
sliver() turns a box widget into a sliver, while sliverPadding,
sliverSafeArea, sliverOpacity, sliverVisibility and sliverIgnorePointer
wrap a widget that is already a sliver.
Validator Exxension #
Every validator returns the message to show under the field, or null when the
value is valid. The format checks stay quiet on an empty field, so requiredError
decides on its own whether a field is mandatory,
TextFormField(
validator: (value) => value.requiredError() ?? value.emailError(),
)
Chain a list of rules and the first failing one wins,
TextFormField(
validator: <FormFieldValidator<String>>[
(value) => value.requiredError('Zorunlu alan.'),
(value) => value.emailError(),
(value) => value.minLengthError(10),
].toValidator(),
)
requiredError, emailError, urlError, phoneError, numericError,
passwordError, minLengthError, maxLengthError, lengthBetweenError,
matchError and patternError all take an optional message, so they translate
into any language.
Layout Snapper #
You can dump the widget tree as JSON while debugging,
// context.printLayout() is the same as
// debugPrint(LayoutSnapper.snapshotOf(context))
context.printLayout();
context.printLayout(maxDepth: 5); // only a few levels deep
// context.layoutSnapshot() hands you the JSON instead of printing it
Clipboard.setData(ClipboardData(text: context.layoutSnapshot()));
// Or reach for the tool itself, for the whole running app
debugPrint(LayoutSnapper.snapshot());
// Or drop the ready-made debug button into your Scaffold.
// It leaves its hero tag empty, so it can sit next to a real FloatingActionButton.
floatingActionButton: LayoutSnapperButton(
onSnapshot: (json) => Clipboard.setData(ClipboardData(text: json)),
),