flutterx_utils 2.0.0
flutterx_utils: ^2.0.0 copied to clipboard
Flutter toolkit of typed extensions, JSON helpers, mutex and cancellation, tagged logging and layout widgets shared by the flutterx packages.
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