New package: exui - Build Flutter UIs faster with less code, same performance, pure Dart and Flutter

A practical toolkit for Flutter UI development, focused on saving time, reducing boilerplate, and writing layout code that’s readable, consistent, and fun.

Whether you’re working on layout, spacing, visibility, or sizing, exui gives you expressive helpers for the most common tasks, with zero dependencies and seamless integration into any codebase.

Here are just a few examples:


:straight_ruler: Padding

With exui:

Text("Hello").paddingAll(16)

Without:

Padding(
  padding: EdgeInsets.all(16),
  child: Text("Hello"),
)

With additional extensions for quickly adding specific padding: paddingHorizontal, paddingVertical, paddingOnly, paddingSymmetric, paddingLeft, paddingRight, paddingTop, paddingBottom


:up_down_arrow: Gaps

exui gaps are more performant than the gap package — they use native SizedBox widgets with no runtime checks or context detection. Just pure Dart and Flutter for clean, zero-overhead spacing.
With exui:

Column(
  children: [
    Text("A"),
    16.gapColumn,
    Text("B"),
  ],
)

Without:

Column(
  children: [
    Text("A"),
    SizedBox(height: 16),
    Text("B"),
  ],
)

With additional extensions for quickly adding specific gap values: gapRow, gapColumn, gapVertical, gapHorizontal etc.


:eye: Visibility

With exui:

Text("Visible?").visibleIf(showText)

Without:

showText ? Text("Visible?") : const SizedBox.shrink()

:construction: Constraints

With exui:

Image.asset("logo.png").maxWidth(200)

Without:

ConstrainedBox(
  constraints: BoxConstraints(maxWidth: 200),
  child: Image.asset("logo.png"),
)

https://pub.dev/packages/exui


All exui Extensions:

Emojis only added to distinguish easily between extensions

Layout Manipulation

:straight_ruler: padding - Quickly Add Padding
:bullseye: center - Center Widgets
↔️ expanded - Fill Available Space
:dna: flex - fast Flexibles
:triangular_ruler: align - Position Widgets
:round_pushpin: positioned - Position Inside a Stack
:white_square_button: intrinsic - Size Widgets
:minus: margin - Add Outer Spacing

Layout Creation

:up_down_arrow: gap - Performant gaps
:brick: row / column - Rapid Layouts
:compass: row* / column* - Rapid Aligned Layouts
:ice: stack - Overlay Widgets

Visibility, Transitions & Interactions

:eye: visible - Conditional Visibility
:fog: opacity - Widget Transparency
:mobile_phone: safeArea - SafeArea Padding
:backhand_index_pointing_up: gesture - Detect Gestures
:superhero: hero - Shared Element Transitions

Containers & Effects

:package: sizedBox - Put in a SizedBox
:construction: constrained - Limit Widget Sizes
:red_square: coloredBox - Wrap in a Colored Box
:artist_palette: decoratedBox - Borders, Gradients & Effects
:scissors: clip - Clip Widgets into Shapes
:mirror: fittedBox - Fit Widgets

Click here to see the full documentation

2 Likes

Isn’t this using functions to build Widgets, which is not recommended?

Functions are “not recommended” because it does not create “another widget” / using the same build method. Therefore if you overuse it (i.e. everything is done using helper methods), it will rebuild the whole widget tree instead of optimizing rebuilds to only small parts of the widget tree. Therefore separate widgets are recommended and generally everyone should use that approach.

But when separating UI to Widgets + using it as a utility does not have these problems.
If you simply use separate widgets and combining these utilities you will not rebuild the whole tree because although the extensions are used, they are inside of a Widget that is not rebuilt.

Yep. Small utils are OK. Since they are part of the widget in the same way as widget wrapped something would be. Therefore no difference.

1 Like

I don’t think this is true.

Check this example:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

final class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

final class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

final class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            //_UnrelatedWidget(),
            SizedBox().unrelatedWidget(),
            Text('$_counter', style: Theme.of(context).textTheme.headlineMedium),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

final class _UnrelatedWidget extends StatelessWidget {
  const _UnrelatedWidget({super.key});

  @override
  Widget build(BuildContext context) {
    print("I'm being built by Flutter");

    return const Text('You have pushed the button this many times:');
  }
}

extension SizedBoxExtension on SizedBox {
  Widget unrelatedWidget() {
    print("I'm being built by extension");
    return const Text('You have pushed the button this many times:');
  }
}

There are 3 cases here:

  1. Using the commented const _UnrelatedWidget() (with const)
  2. Using the commented _UnrelatedWidget() (without const)
  3. Using the .unrelatedWidget() extension method

Every time I press the (+) button (which triggers a rebuild through setState), the UnrelatedWidget is rebuilt except when using the const constructor.

Even though the extension method uses a const Text, the method itself run (and there is no need to run at all, since his reason to exist didn’t changed).

At least in this sample, the optimization brought by const constructors are lost.

For me, not running build is a HUGE deal, and all thanks to a simple const keyword (which people say it is not even needed in the language and/or doesn’t make any difference).

Am I missing something?

2 Likes

They even removed the prefer_const_constructors from the default linter rules. I don’t understand this either, it clearly speeds up the rebuild process, even if by a minuscule amount. Maybe they’re planning to make the compiler figure out if the widget is supposed to be const and they just put the cart before the horse?