fabrik_utils
Every app ends up with a utils.dart holding the same handful of helpers:
something that turns a DateTime into “2 hours ago”, something that converts
a string to snake_case, a debouncer for the search field.
fabrik_utils is that file, written once and tested properly.
'XMLHttpRequest'.snakeCase // 'xml_http_request'postedAt.timeAgo // '2 hours ago'formatDuration(elapsed) // '01:23:45'Installation
Section titled “Installation”dependencies: fabrik_utils: ^0.2.0import 'package:fabrik_utils/fabrik_utils.dart';String casing
Section titled “String casing”Ten casing styles, all built on the same word-splitting logic. It understands delimiters, camelCase boundaries, and acronyms:
'hello world'.titleCase // 'Hello World''helloWorld'.snakeCase // 'hello_world''user_name'.camelCase // 'userName''http_request'.pascalCase // 'HttpRequest''Hello World'.kebabCase // 'hello-world''Hello World'.constantCase // 'HELLO_WORLD''hello world'.dottedCase // 'hello.world''hello world'.pathCase // 'hello/world''hello WORLD'.sentenceCase // 'Hello world''hello world'.headerCase // 'Hello-World'Runs of consecutive capitals are kept together, so acronyms survive:
'XMLHttpRequest'.snakeCase // 'xml_http_request''APIKey'.snakeCase // 'api_key''parseJSON'.camelCase // 'parseJson'There is also a null-safe blank check:
String? input;input.isNullOrBlank; // true — null, empty, or whitespace only' '.isNullOrBlank; // trueDates and times
Section titled “Dates and times”Relative time
Section titled “Relative time”timeAgo reads in both directions, so it works for timestamps and for
scheduled events:
postedAt.timeAgo // '5 mins ago' · 'yesterday' · '2 months ago'dueAt.timeAgo // 'in 5 mins' · 'tomorrow' · 'in 3 days'Formatting
Section titled “Formatting”Named getters over intl, so you do not memorise format strings:
date.isoDate // '2026-06-15'date.dayMonthYear // '15 Jun 2026'date.fullWithWeekday // 'Monday, June 15, 2026'date.shortDateTime // 'Jun 15, 2026 11:40 PM'date.time12Hour // '11:40 PM'date.hourMinute24h // '23:40'date.weekdayName // 'Monday'date.monthShort // 'Jun'Checks and math
Section titled “Checks and math”date.isTodaydate.isTomorrowdate.isYesterdaydate.isWeekenddate.isBetween(start, end) // inclusive
date.startOfDay // 00:00:00.000date.endOfDay // 23:59:59.999date.startOfWeek // midnight on the most recent MondayDurations
Section titled “Durations”formatDuration(const Duration(seconds: 3665)) // '01:01:05'formatDuration(const Duration(minutes: 4)) // '04:00'formatDuration(const Duration(seconds: -65)) // '-01:05'Hours appear only when non-zero, unless you force them:
formatDuration(const Duration(seconds: 90), alwaysShowHours: true); // '00:01:30'For custom timer UIs where each unit is its own widget, splitDuration
returns the padded components:
final (:hours, :minutes, :seconds) = splitDuration(3665);// '01', '01', '05'Debounce and throttle
Section titled “Debounce and throttle”The two are easy to confuse:
| Fires | Use for | |
|---|---|---|
| Debounce | After input stops for a given duration | Search-as-you-type, autosave |
| Throttle | At most once per duration, immediately | Button taps, scroll handlers |
Debounce
Section titled “Debounce”final debounce = Debounce(duration: const Duration(milliseconds: 300));
void onSearchChanged(String query) { debounce.add(() => performSearch(query));}
@overridevoid dispose() { debounce.close(); super.dispose();}Use maxWait when continuous input should not defer execution forever:
Debounce( duration: const Duration(milliseconds: 300), maxWait: const Duration(seconds: 2), // runs at least every 2s while typing);cancel() drops a pending call without closing the debouncer, which is useful
when a screen is popped mid-edit.
Throttle
Section titled “Throttle”final throttle = Throttle(duration: const Duration(seconds: 1));
ElevatedButton( onPressed: () => throttle.add(submitOrder), // ignores rapid double taps child: const Text('Place order'),);Both expose their state as a stream, so you can drive UI from it:
throttle.listen((status) { setState(() => _buttonEnabled = status.isIdle);});Infinite scroll
Section titled “Infinite scroll”final controller = ScrollController();
controller.addListener(() { if (isApproachingScrollEnd(controller) && !isLoading) { loadNextPage(); }});The default threshold triggers at 70% of the scroll extent; pass
scrollOffsetThreshold to change it. It returns false when the controller
has no clients, so it is safe to call before the first layout.
API reference
Section titled “API reference”StringX on String
Section titled “StringX on String”| Member | Example |
|---|---|
titleCase | Hello World |
headerCase | Hello-World |
pascalCase | HelloWorld |
camelCase | helloWorld |
snakeCase | hello_world |
constantCase | HELLO_WORLD |
kebabCase | hello-world |
dottedCase | hello.world |
pathCase | hello/world |
sentenceCase | Hello world |
capitalizeFirst | Hello world |
NullableStringX on String? adds isNullOrBlank.
DateTimeX on DateTime
Section titled “DateTimeX on DateTime”| Group | Members |
|---|---|
| Checks | isToday, isTomorrow, isYesterday, isWeekday, isWeekend, isBetween |
| Math | startOfDay, endOfDay, startOfWeek |
| Relative | timeAgo |
| Formatters | isoDate, isoDateTime, dayMonthYear, shortMonthDay, monthDay, fullWithWeekday, fullWithWeekdayShort, fullDateTime, shortDateTime, time12Hour, hourMinute12h, hourMinute24h, weekdayName, weekdayShort, monthFull, monthShort |
Helpers
Section titled “Helpers”| Function | Returns |
|---|---|
formatDuration(duration, {alwaysShowHours}) | String |
splitDuration(totalSeconds) | ({String hours, String minutes, String seconds}) |
isApproachingScrollEnd(controller, {scrollOffsetThreshold}) | bool |
Debounce<T> and Throttle<T>
Section titled “Debounce<T> and Throttle<T>”| Member | Description |
|---|---|
add(fn) | Queue (debounce) or attempt (throttle) a call |
debounce(fn) / throttle(fn) | Named equivalents of add |
cancel() | (Debounce) Drop the pending call, stay open |
close() | Cancel timers and close the stream |
isIdle / isWaiting / isBusy | Current state |
listen(onData) | Subscribe to status changes |