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)),
),

Libraries

exxen