Skip to content

fabrik_utils

pub.dev

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'
dependencies:
fabrik_utils: ^0.2.0
import 'package:fabrik_utils/fabrik_utils.dart';

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; // true

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'

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'
date.isToday
date.isTomorrow
date.isYesterday
date.isWeekend
date.isBetween(start, end) // inclusive
date.startOfDay // 00:00:00.000
date.endOfDay // 23:59:59.999
date.startOfWeek // midnight on the most recent Monday
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'

The two are easy to confuse:

FiresUse for
DebounceAfter input stops for a given durationSearch-as-you-type, autosave
ThrottleAt most once per duration, immediatelyButton taps, scroll handlers
final debounce = Debounce(duration: const Duration(milliseconds: 300));
void onSearchChanged(String query) {
debounce.add(() => performSearch(query));
}
@override
void 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.

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);
});
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.

MemberExample
titleCaseHello World
headerCaseHello-World
pascalCaseHelloWorld
camelCasehelloWorld
snakeCasehello_world
constantCaseHELLO_WORLD
kebabCasehello-world
dottedCasehello.world
pathCasehello/world
sentenceCaseHello world
capitalizeFirstHello world

NullableStringX on String? adds isNullOrBlank.

GroupMembers
ChecksisToday, isTomorrow, isYesterday, isWeekday, isWeekend, isBetween
MathstartOfDay, endOfDay, startOfWeek
RelativetimeAgo
FormattersisoDate, isoDateTime, dayMonthYear, shortMonthDay, monthDay, fullWithWeekday, fullWithWeekdayShort, fullDateTime, shortDateTime, time12Hour, hourMinute12h, hourMinute24h, weekdayName, weekdayShort, monthFull, monthShort
FunctionReturns
formatDuration(duration, {alwaysShowHours})String
splitDuration(totalSeconds)({String hours, String minutes, String seconds})
isApproachingScrollEnd(controller, {scrollOffsetThreshold})bool
MemberDescription
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 / isBusyCurrent state
listen(onData)Subscribe to status changes