kh_utils¶
Stateless helpers for khflutterlib: form validators, value formatters (date/currency/number/phone), country-aware phone numbers, and a leveled logger.
Pure Dart — no Flutter dependency — so it can be used and tested without
the Flutter SDK (see PHASE_0_ARCHITECTURE.md §19).
Why you'd reach for this: every one of these is boilerplate you'd
otherwise write per-screen — a required-field check, a "2 days ago"
timestamp, a print() you meant to remove before shipping. kh_utils
gives you the same handful of well-tested implementations everywhere
instead of a slightly different version in every screen/app. If you're
skimming this for a hackathon: KhValidators + KhFormatters cover
almost every form/display need you'll hit — jump to their sections
below, or run the packages/example gallery app's "Validators
(kh_utils)"/"Formatters (kh_utils)" screens to see them live.
Status¶
Available. Version 0.1.2+1, shipped in release train khflutterlib-v0.4.3.
Exports:
KhValidator— the validator function shape (String? Function(String? value))KhValidators—required,email,phone,url,minLength,maxLength,exactLength,numeric,min,max,range,positive,alphanumeric,alpha,noWhitespace,pattern,combineKhFormatters—shortDate,longDate,relative,currency,number,phoneKhPhoneRegion— country-aware phone validation, normalization to E.164, and display formatting (ng,us, or your own)KhLogger/KhLogLevel/KhLogSink— leveled logging facade (KhLogger.instance)
Usage¶
dependencies:
kh_utils:
git:
url: https://github.com/msflib/flutter.git
path: packages/kh_utils
ref: khflutterlib-v0.4.3 # use the same release tag for every kh_* package
Validators¶
Every KhValidators method returns a KhValidator
(String? Function(String? value)) — the same shape as Flutter's
FormFieldValidator<String>, so it plugs directly into
TextFormField.validator or KhTextField with no wrapping:
import 'package:kh_utils/kh_utils.dart';
TextFormField(
validator: KhValidators.combine([
KhValidators.required(),
KhValidators.email(),
]),
)
Most validators pass on an empty value by design (e.g. email,
minLength, numeric, pattern) — they check the shape of a value
that's present, not whether one was provided. Combine with required()
first in a combine([...]) chain to also enforce non-emptiness:
final validatePassword = KhValidators.combine([
KhValidators.required(message: 'Password is required.'),
KhValidators.minLength(8, message: 'Use at least 8 characters.'),
]);
Each validator accepts an optional message (or a required one for
pattern, since there's no sensible generic default):
min/max/range/positive also treat a non-numeric value as a
failure (there's no sensible bound check on something that isn't a
number) — combine with numeric() first if you want a message that
distinguishes "not a number" from "out of range":
final validateAge = KhValidators.combine([
KhValidators.required(),
KhValidators.numeric(),
KhValidators.range(18, 120, message: 'Enter an age between 18 and 120.'),
]);
Formatters¶
import 'package:kh_utils/kh_utils.dart';
final date = DateTime(2026, 9, 29);
KhFormatters.shortDate(date, locale: 'en_US'); // '9/29/2026'
KhFormatters.longDate(date, locale: 'en_US'); // 'September 29, 2026'
KhFormatters.relative(someDate); // '2 days ago' / 'in 3 hours' / 'Just now'
KhFormatters.currency(1234.5, locale: 'en_US', symbol: r'$'); // '$1,234.50'
KhFormatters.currency(1250000, locale: 'en_NG', symbol: '₦',
decimalDigits: 0); // '₦1,250,000' (no kobo)
KhFormatters.currency(1250000, locale: 'en_NG', symbol: '₦',
compact: true); // '₦1.25M'
KhFormatters.currency(1250000, locale: 'en_NG', symbol: '₦',
compact: true, lowercaseSuffix: true); // '₦1.25m'
KhFormatters.number(1234567); // '1,234,567'
KhFormatters.phone('5551234567'); // '(555) 123-4567'
Every formatter takes an optional locale; without one, intl's current
default locale is used. Pass symbol to currency as well: without it,
intl prints the currency's ISO code (for example USD1,234.50) instead
of a symbol.
relative falls back to shortDate once the difference is a week or
more, in either direction — "47 days ago" is less useful than an actual
date at that point. phone is a display convenience for common 10/11
digit lengths only; anything else is returned unchanged. Neither
currency nor number perform currency conversion — they format a
number you already have in the target currency's units.
Compact currency rounds to about three significant digits (intl's
behaviour), so decimalDigits has no effect there: 1,255,000 shows as
₦1.25M. Use the full format wherever the exact amount matters, such as
checkout totals.
Phone numbers by country¶
KhPhoneRegion knows how one country writes, validates and stores phone
numbers. KhPhoneRegion.ng (Nigerian mobile numbers) and
KhPhoneRegion.us are built in:
// Validate what people actually type: 0803 123 4567, 08031234567,
// +234 803 123 4567, 2348031234567, ...
KhTextField(
keyboardType: TextInputType.phone,
validator: KhValidators.phone(region: KhPhoneRegion.ng),
);
// Store one canonical form (E.164):
KhPhoneRegion.ng.normalize('0803 123 4567'); // '+2348031234567'
KhPhoneRegion.ng.normalize('0603 123 4567'); // null (not a mobile range)
// Display it back:
KhFormatters.phone('+2348031234567', region: KhPhoneRegion.ng);
// '0803 123 4567'
KhFormatters.phone('+2348031234567', region: KhPhoneRegion.ng,
international: true); // '+234 803 123 4567'
KhPhoneRegion.ng is meant for the 070, 080, 081, 090 and 091
mobile ranges. Its pattern ([789][01] followed by eight digits, after
the trunk 0) also accepts 071, so don't rely on it to reject that
prefix. Landlines have variable lengths and aren't
accepted. For another country, use the constructor: a country code, an
optional trunk prefix, a pattern for the national number, and two
display patterns where each # is a digit. Without a region,
KhValidators.phone still requires E.164 and KhFormatters.phone keeps
its US-style grouping, so existing code is unaffected.
Logger¶
import 'package:kh_utils/kh_utils.dart';
KhLogger.instance.debug('Fetching users...');
KhLogger.instance.info('Signed in', tag: 'Auth');
KhLogger.instance.warning('Retrying request', error: exception);
KhLogger.instance.error('Failed to load profile', error: exception, stackTrace: stackTrace);
By default, debug/info are hidden once kh_core's
KhEnvironmentConfig.current.enableLogging is false (typically your
production config) — warning/error always show. Override the
threshold or where output goes at bootstrap:
KhLogger.instance.configure(
minLevel: KhLogLevel.info,
sink: (level, message) {
// Forward to a crash-reporting service in addition to the console,
// for example — the default sink uses dart:developer's log().
},
);
Each line reaches the sink as [LEVEL] [tag] message, followed by
separate error: ... and stack-trace lines when you pass them.