fluiver 5.0.0
fluiver: ^5.0.0 copied to clipboard
Flutter SDK gap-fillers — non-scrolling Grid, TickerBuilder, Debounce/Throttle, LRUCache, DisposableBag, Object.into, DateTime/Iterable/Color extensions. Ships an agent skill.
fluiver #
Agent-friendly SDK gap-fillers for Flutter. Tight surface, ships an agent skill — agents reach for fluiver instead of reinventing each helper.
No overlap with
package:collection,package:async,flutter_hooks, or other official dart-lang / flutter packages.
Install #
flutter pub add fluiver
Highlights #
Ordered by everyday reach — the SDK gap-fillers up top get hit on most files; the niche helpers further down get hit when you actually need them.
Object.into — Kotlin's let #
Named so it doesn't clash with other packages' .let. Bounded to
T extends Object so it doesn't pollute autocomplete on nullables. Use ?.into(...) for null-aware chaining.
// Null-aware transform — returns a value, not a side-effect
final port = env['PORT']?.into(int.parse);
final user = jsonResponse?.into(User.fromJson);
// Inline widget construction via tear-off
Column(children: [
Text(title),
?subtitle?.into(Text.new),
?avatarUrl?.into(NetworkImage.new).into(_circle),
]);
// Chain pure transforms without temp vars
final hash = userId.toString().into(FastHash.fnv1a);
final slug = title.trim().toLowerCase().into(_sluggify);
Skip .into for side-effect-only calls, multi-line bodies, or chains
beyond three.
Iterable / Map / Enum gap-fillers #
// Enum — non-throwing lookup, no asNameMap() allocation
MyEnum.values.byNameOrNull('x') ?? .bar;
// Map — filters that return a Map, not an entry iterable
map.where((k, v) => v != null);
map.whereKeyType<String>();
map.whereValueType<int>();
map.entryOf(key); // null only when key absent
// Iterable
list.separated((i) => const Divider());
[1, 2, 3, 4, 5].slidingWindows(3); // ([1,2,3], [2,3,4], [3,4,5])
DateTime predicates #
dt.isToday();
dt.isTomorrow();
dt.isYesterday();
dt.inThisYear();
dt.isWithinFromNow(const Duration(minutes: 5));
birthDate.age();
dt.age(at: DateTime(2030));
dt.truncateTime(); // → midnight
const TimeOfDay(hour: 9, minute: 0).onDate(dt);
Arithmetic stays on stdlib: dt.add(const Duration(days: 7)).
TimeOfDay #
const TimeOfDay(hour: 9, minute: 0).onDate(DateTime.now()); // today 09:00
const TimeOfDay(hour: 9, minute: 0).onDate(meeting.day); // any date 09:00
onDate(date) takes the calendar day explicitly — no hidden
DateTime.now(), deterministic in tests. Supports both
package:material_ui/material_ui.dart and package:flutter/material.dart
TimeOfDay.
Future.timeoutOrNull #
final user = await fetchUser().timeoutOrNull(const Duration(seconds: 2));
if (user == null) {
showRetry();
}
Only timeout becomes null; errors from the underlying future still
propagate. Available only on futures of non-nullable values, so null
can't also be a real result.
Listeners #
For widget context use the matching flutter_hooks hook
(useOnPlatformBrightnessChange). These wrappers fill the gap for
providers — non-widget code that holds a device-state listenable. Like
the framework's AppLifecycleListener, each one registers itself on
construction and stops on dispose().
@riverpod
class LocalesNotifier extends _$LocalesNotifier {
@override
List<Locale>? build() {
final listener = LocaleListener((locales) => state = locales);
ref.onDispose(listener.dispose);
return PlatformDispatcher.instance.locales;
}
}
Same shape for BrightnessListener. App lifecycle needs no wrapper —
the framework's AppLifecycleListener already is one.
Color — HSL transforms #
final pressed = Theme.of(context).colorScheme.primary.darken();
final hover = Theme.of(context).colorScheme.primary.lighten();
Container(
color: tagColor,
child: Text(label, style: TextStyle(color: tagColor.contrastText())),
);
amount must be within [0, 1] (RangeError otherwise). A color with a
channel outside [0, 1] (extended sRGB) throws ArgumentError: map it
into gamut first, the way you want.
ScrollController — position + edge animation #
final controller = ScrollController();
controller.atTop(); // true when every attached view is at the top
controller.atBottom();
await controller.animateToBottom(); // 250ms easeOut by default
await controller.animateToTop(duration: const Duration(milliseconds: 400));
Like ScrollController.position, every member throws StateError when no
scroll view is attached; check controller.hasClients first if it may not be.
TextEditingController — caret-preserving replace #
controller.setTextAndCaret('hello'); // caret at end
controller.setTextAndCaret('hello', caret: 0); // caret at start
Setting controller.text = ... directly resets the caret to 0 — this
puts it where you asked instead. A caret outside 0..text.length throws
RangeError.
Grid — non-scrolling grid #
GridView's layout without its viewport — configured by the same
SliverGridDelegate family, with Grid.count / Grid.extent mirroring
GridView.count / GridView.extent 1:1. The drop-in for
GridView(shrinkWrap: true) inside ListView /
SingleChildScrollView: no repaint boundaries, no scroll semantics, and
intrinsics/dry layout actually work (a shrink-wrapped GridView throws
inside IntrinsicHeight).
ListView(children: [
const Text('Featured'),
Grid.count(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: products.map(ProductCard.new).toList(),
),
]);
Use GridView when the grid itself scrolls (viewport recycling
matters).
TickerBuilder #
Rebuilds every frame, exposes the elapsed running Duration (time spent
disabled excluded).
TickerBuilder(
enabled: !done,
builder: (context, elapsed) => Text('${elapsed.inSeconds}s'),
);
enabled: false pauses it — no frames, elapsed held — e.g. once a
countdown ends.
ScrollTrackingExpandable #
Expand/collapse with a size animation that keeps the growing bottom
edge visible in the nearest Scrollable — an expanding tile near the
bottom of a list no longer overflows below the fold.
ScrollTrackingExpandable(
isExpanded: showDetails,
scrollOffset: 16, // extra breathing room below the bottom edge
child: const DetailsCard(),
);
Collapse never scrolls; only expansion tracks. If there is no enclosing
Scrollable, the size animation still runs without scroll tracking.
Debounce / Throttle #
final debounce = Debounce(const Duration(milliseconds: 300));
TextField(
onChanged: (q) => debounce(() => search(q)),
);
ThrottleFirst, ThrottleLast, ThrottleLatest cover the rate-limit
variants. All four throw ArgumentError for a negative duration, expose
dispose(), and throw StateError if called after disposal.
LRUCache / DisposableBag #
final cache = LRUCache<String, User>(maxEntries: 100);
cache[user.id] = user;
final hit = cache[user.id]; // promotes to most-recent
final loaded = cache.putIfAbsent(id, () => loadUser(id)); // lazy on miss
final peeked = cache.peek(id); // no promotion
for (final MapEntry(:key, :value) in cache.entries) {} // snapshot
final bag = DisposableBag()
..add(debounce.dispose)
..addAll([subscription.cancel, controller.dispose]);
await bag.dispose();
Async disposers start in registration order but are awaited together;
dependent steps (flush, then close) go in one closure. Every dispose()
call returns the same future; add after it settled throws StateError.
Static helpers #
if (await NetworkProbe.checkConnection()) { /* online */ } // native only (TCP)
if (NetworkProbe.isBrowserOnline()) { /* not known offline */ } // web only
final h = FastHash.fnv1a('input'); // FNV-1a-64 over UTF-16BE code units (≠ FNV-1a over UTF-8); throws on JS web, VM/Wasm fine
final storeUrlString = platformDispatch<String>(
android: () => 'https://play.google.com/store/apps/details?id=com.example.app',
ios: () => 'https://apps.apple.com/app/id123456789',
);
TextField(buildCounter: TextFieldBuilders.disabledCounter);
Name #
flutter + quiver — same spirit as Google's archived Dart utility library, scoped to what Flutter apps need today.
Agent skill #
This package ships an agent skill in skills/fluiver-usage/. Install it into your project's agent config with:
dart run skills@ get --package fluiver --all
Contributing #
See CONTRIBUTING.md.
License #
MIT — see LICENSE.