fluiver 3.1.0
fluiver: ^3.1.0 copied to clipboard
Flutter utility library — debounce/throttle, observers, FlexGrid, DateTime predicates, and SDK gap-fillers.
fluiver #
SDK gap-fillers for Flutter. Every API passes the gap test — the Dart or Flutter SDK should have shipped it and didn't. No shortcut sugar, no package:collection overlap.
- Debounce/throttle helpers with proper disposal
FlexGrid— non-scrolling grid safe insideListView/SingleChildScrollViewTickerBuilder— per-frame rebuild with elapsedDuration- System observers (
Locale,Brightness,AppLifecycle) for provider code DateTimepredicates (isToday,isTomorrow,age(), …)Mappredicates the SDK only has onIterableEnum.byNameOrNull(the SDK only ships throwingbyName)Object.let(Kotlin scope function)LRUCache<K, V>,DisposableBag— common patterns the SDK doesn't shipFastHash.fnv1a,NetworkProbe.hasConnection,platformDispatch,TextFieldBuilders.disabledCounterColor.darken/.lighten/.contrastText,ScrollController.atTop/.atBottom/.animateToTop/.animateToBottom,Future.timeoutOrNull,Iterable.windowed,TextEditingController.setTextAndCaret
dependencies:
fluiver: ^3.1.0
Upgrading from 2.x or earlier 3.x? The 3.x line trims shortcut extensions and surface APIs aggressively. See CHANGELOG.md for full removal + migration tables.
Install #
dart pub add fluiver
import 'package:fluiver/fluiver.dart';
Highlights #
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 expose dispose().
FlexGrid — non-scrolling grid inside scrollables #
ListView(
children: [
const Text('Featured'),
FlexGrid(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: products.map(ProductCard.new).toList(),
),
],
);
Drop-in replacement for GridView(shrinkWrap: true) without the
performance penalty. Custom RenderObject — does not scroll itself.
Observers #
For widget context use the matching flutter_hooks hook
(useOnAppLifecycleStateChange, useOnPlatformBrightnessChange). These
wrappers fill the gap in non-widget code.
@riverpod
class LocalesNotifier extends _$LocalesNotifier {
@override
List<Locale>? build() {
final observer = LocaleObserver((locales) => state = locales);
WidgetsBinding.instance.addObserver(observer);
ref.onDispose(() => WidgetsBinding.instance.removeObserver(observer));
return PlatformDispatcher.instance.locales;
}
}
Same shape for BrightnessObserver / AppLifecycleObserver.
DateTime predicates #
dt.isToday;
dt.isTomorrow;
dt.inThisYear;
dt.isWithinFromNow(const Duration(minutes: 5));
birthDate.age();
dt.withTimeOfDay(const TimeOfDay(hour: 9, minute: 0));
For arithmetic use stdlib: dt.add(const Duration(days: 7)).
TimeOfDay #
const TimeOfDay(hour: 9).onDate(DateTime.now()); // today 09:00
const TimeOfDay(hour: 9).onDate(meeting.day); // any date 09:00
SDK gap-fillers #
// Enum — non-throwing lookup; chain ?? for a fallback
MyEnum.values.byNameOrNull('foo');
MyEnum.values.byNameOrNull('x') ?? .bar;
// Map — what Iterable already has
map.firstWhereOrNull((k, v) => v.isActive);
map.whereKeyType<String>();
map.whereValueType<int>();
// Iterable
list.separated((i) => const Divider());
[1, 2, 3, 4, 5].windowed(3); // ([1,2,3], [2,3,4], [3,4,5])
Object.let — Kotlin scope function #
Bounded to T extends Object so it doesn't pollute autocomplete on
nullables. Use ?.let(...) for null-aware chaining.
// 1. Null-aware transform — returns a value, not a side-effect
final port = env['PORT']?.let(int.parse);
final user = jsonResponse?.let(User.fromJson);
// 2. Inline widget construction via tear-off
Column(children: [
Text(title),
?subtitle?.let(Text.new),
?avatarUrl?.let(NetworkImage.new),
]);
// 3. Chain pure transformations without temp vars
final hash = userId.toString().let(FastHash.fnv1a);
final slug = title.trim().toLowerCase().let(_sluggify);
// 4. Conditional spread of children
Column(children: [
Text(title),
...?subtitle?.let((s) => [const SizedBox(height: 4), Text(s)]),
]);
// 5. Static method as transform — no lambda
final id = rawId?.let(int.tryParse);
Don't reach for .let for side-effect-only calls (returns a value —
wasted), multi-line bodies (use a temp), or chains beyond three.
LRUCache / DisposableBag #
final cache = LRUCache<String, User>(maxEntries: 100);
cache['alice'] = user;
final hit = cache['alice']; // promotes to most-recently-used
final bag = DisposableBag()
..add(debounce.dispose)
..add(subscription.cancel)
..add(controller.dispose);
await bag.dispose();
Static helpers #
if (await NetworkProbe.hasConnection()) { /* online */ }
final h = FastHash.fnv1a('input'); // FNV-1a int64 (VM only, not Web)
final storeUrl = platformDispatch<Uri>(
android: () {
return Uri.parse('https://play.google.com/store/apps/details?id=com.example.app');
},
ios: () {
return Uri.parse('https://apps.apple.com/app/id123456789');
},
);
TextField(buildCounter: TextFieldBuilders.disabledCounter);
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)),
);
ScrollController — position + edge animation #
final controller = ScrollController();
controller.atTop; // false when no client attached, then true at top
controller.atBottom;
await controller.animateToBottom(); // 250ms easeOut by default
await controller.animateToTop(duration: const Duration(milliseconds: 400));
Future.timeoutOrNull #
final user = await fetchUser().timeoutOrNull(const Duration(seconds: 2));
if (user == null) {
showRetry();
}
Errors from the underlying future still propagate — only the timeout
itself is converted to null.
TextEditingController.setTextAndCaret #
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.
LLM rule #
A consumer-side rule ships at rules/flutter-fluiver.md. Drop it into
your agent's rules directory (~/.claude/rules/, .cursor/rules/, or
the AntiGravity equivalent) so the agent reaches for fluiver APIs
instead of reinventing them.
License #
MIT.