flutterx_utils
Typed extensions, JSON helpers, mutex and cancellation, tagged logging and layout widgets for Flutter, shared by the flutterx packages. It only offers what the Flutter SDK does not.
Installation
flutter pub add flutterx_utils
import 'package:flutterx_utils/flutterx_utils.dart';
JSON and maps
Map reads values through three families: require* returns or throws, opt* returns null, get*
returns a fallback. A key that is missing or null counts as absent; a value of the wrong type throws
an ArgumentError naming the key.
class User with DTO {
final String name;
final int age;
final Role role;
final List<Tag> tags;
User.fromJson(JsonObject json)
: name = json.requireString('name'),
age = json.getInt('age', fallback: 18),
role = json.getEnum('role', Role.values, fallback: Role.guest),
tags = json.getJsonList('tags', Tag.fromJson);
@override
JsonObject toJson() => {'name': name, 'age': age, 'role': role.name, 'tags': tags.toJsonList()};
}
Bool,IntandDoublealso accept strings ('true','42');Intrejects values with a fractional part. A malformed string throws aFormatExceptioninrequire*and counts as absent inopt*andget*.Enumreads by name, or by index withindex: true, andignoreCase: trueignores case. An unknown name, a missing key or an out-of-range index yields null inopt*and the fallback inget*;require*throws anArgumentErrornaming the key.MapEnumreads through aMap<String, E>, withlower: trueto lowercase the input first.requireJsonObject,requireJsonList,requireListandrequireMapread nested data, each withopt*; theget*of lists and maps give an empty collection when absent.getRaw<E>returns the value as-is;require<X, T>andopt<X, T>apply your own conversion.DTO.toString()prints the fields fromtoJson()in debug builds and onlyType(…)in release.hashMap(map)hashes by content, independent of key order.List<Enum>hasrequireFromName,fromNameandoptFromName.
Concurrency
Mutex runs one async task at a time, waiters in arrival order:
final mutex = Mutex();
await mutex.lock(() async => save());
MutexRun throttles or debounces calls. A call that does not run gets its fallback: a value, a
Future, or null for a nullable result; without one it fails with ConcurrentExecutionError. Any
other fallback type throws an ArgumentError at once.
final search = MutexRun.last(const Duration(milliseconds: 300)); // debounce
final results = await search(() => api.search(text), fallback: null);
final submit = MutexRun.first(); // ignore taps while running
submit(() => api.send(form), fallback: null);
| Factory | Behaviour |
|---|---|
MutexRun.once() |
Runs until the first success; later calls get the fallback until invalidate(). |
MutexRun.first() |
Throttle: runs only when nothing is running. |
MutexRun.every(delay) |
Like first, plus the fallback within delay of the last completed run. |
MutexRun.last(delay) |
Debounce: after delay only the latest call runs. |
CancellationToken makes one attached future fail with CancelException when cancelled;
CancellationToken.timeout(duration) cancels by itself. One task at a time per token.
final token = CancellationToken();
final data = token.attach(api.load());
token.cancel();
Logging
Mix Loggable into a class or State. Everything is logged in debug builds and nothing in release;
levels are set per type or tag.
class Repo with Loggable {
void load() => logInfo('loading', () => logDebug('from cache'));
}
Loggable.setLevelFor<Repo>(LogLevel.warning);
logRun indents the nested messages; override logInternal to redirect the output.
Extensions
| On | Members |
|---|---|
Brightness |
isDark, negative |
Color |
>>/<< lighten/darken (HSL), * sets the alpha, + paints over, blend, differenceRGB, negative, brightness, hexString, toHSLColor |
Iterable |
interpolate, count, countWhere, distinctBy |
List |
nextElement, updateWhere, updateAllWhere, upsertWhere |
Set |
toggleAdd |
String |
lowerCamelCase, upperCamelCase, snakeCase, capitalized, nonEmptyOrNull |
| any | let |
color * 0.5 replaces the alpha with 0.5; it does not halve the current one. TypedFactoryMap
caches one item per key and requested type; asking for another type replaces the item.
Layout and scroll widgets
SizeObserver reports its child size after the frame, only when it changes, so the callback may
call setState:
SizeObserver(
onSizeChanged: (size) => setState(() => _size = size),
child: const Text('Hello'),
)
It measures with a dry layout by default; wrap a child that does not support it (such as an input
decorator) in SafeDryLayout, or pass useDryLayout: false. constraints
(PartialBoxConstraints) changes the measuring constraints.
ScrollableView scrolls its child in both directions and drives external controllers, so scrollbars
attach to any content:
ScrollableViewScrollbar.both(
controllerX: x,
controllerY: y,
child: ScrollableView(controllerX: x, controllerY: y, child: table),
)
onViewportSize and onChildSize are called after the frame. matchContainerX/Y stretches content
smaller than the viewport.
WidgetDataSync keeps data derived from a widget in sync across rebuilds (sync() in
didUpdateWidget, dispose() in dispose). schedulePostFrame runs a callback after the frame if
called during build, immediately otherwise; scheduleEndOfFrame always waits for the frame end.
Claude Code plugin
The repository ships a plugin with skills that teach Claude Code to integrate the library:
/plugin marketplace add https://gitlab.com/dade.sliep/flutterx_utils
/plugin install flutterx-utils@flutterx-utils
Libraries
- flutterx_utils
- Extensions, JSON helpers, concurrency primitives, logging and layout widgets shared by the flutterx packages.