-
Notifications
You must be signed in to change notification settings - Fork 0
Extensions
A companion package that bridges layrz_ui into the rest of the Layrz ecosystem — i18n, SDK model
conversions, page-transition helpers for go_router, and CDN-hosted fonts. It ships as its own
package on pub.dev, with its own version and release cycle, separate from layrz_ui.
Not to be confused with Platform and Extensions, which documents the
LayrzPlatform enum and the Color/BuildContext extensions that ship inside layrz_ui itself.
This page is about a different, separate package with "Extensions" in its name — one that depends
on layrz_ui, not one layrz_ui depends on.
Repository: goldenm-software/layrz_ui_extensions
layrz_ui is a design system with a deliberately short dependency list — flutter, flutter_svg,
and flutter_material_design_icons, and nothing else. It has no dependency on layrz_sdk,
layrz_i18n, http, or go_router, and that is a hard constraint, not an oversight: pulling in a
data-model package, a translation engine, or a routing package would couple a design system to
concerns it has no business knowing about.
But real apps need those things wired up: translated strings, SDK model conversions, page
transitions integrated with go_router, and fonts served from a CDN instead of Google Fonts.
layrz_ui_extensions is where that wiring lives, so it doesn't have to live in layrz_ui. From the
package's own README:
Adapters and type converters that bridge Layrz ecosystem packages into
layrz_ui, keeping the design system lightweight and decoupled from heavy dependencies.
Each adapter is optional — an app picks up only the ones it needs, and an app using none of them gets zero overhead from this package's dependencies.
Add both packages — layrz_ui_extensions does not replace layrz_ui, it sits alongside it:
dependencies:
layrz_ui: ^0.0.13
layrz_ui_extensions: ^0.0.3The two packages version independently: a layrz_ui_extensions release does not imply a matching
layrz_ui release, and vice versa. layrz_ui_extensions's pubspec.yaml currently requires
layrz_ui: ^0.0.13. The minimum that matters functionally is layrz_ui >= 0.0.9 — the release
that introduced LayrzUiL10n, which the i18n binding below depends on — but the declared
constraint in the published package is ^0.0.13.
This is the package's largest feature. layrz_ui declares its own localization contract,
LayrzUiL10n, with 133 English strings across 17 namespaces (actions, calendar, tables, dual
lists, and so on) — but layrz_ui has no i18n engine dependency, so out of the box every string
is hardcoded English. layrz_ui_extensions routes those lookups through
layrz_i18n instead.
Two pieces make this work:
-
LayrzUiI18n— implementsLayrzUiL10nby mixing in 17 namespace mixins, one perLayrzUiL10nnamespace, each mapping its members to dotted-key lookups (actions.cancel,calendar.monthNext,helpers.duration.days, …) against aLayrzI18nengine instance. -
LayrzUiI18nDelegate— aLocalizationsDelegatethat loads aLayrzUiI18nwrapping your engine.
import 'package:layrz_i18n/layrz_i18n.dart';
import 'package:layrz_ui/layrz_ui.dart';
import 'package:layrz_ui_extensions/layrz_ui_extensions.dart';
Future<void> main() async {
final i18n = LayrzI18n(languages: ['en', 'es', 'pt']);
await i18n.load();
runApp(MyApp(i18n: i18n));
}
class MyApp extends StatelessWidget {
const MyApp({required this.i18n, super.key});
final LayrzI18n i18n;
@override
Widget build(BuildContext context) {
return LayrzApp(
title: 'My App',
theme: LayrzThemeData.light(),
localizationsDelegates: [
LayrzUiI18nDelegate(i18n), // Your translations
const LayrzUiL10nDelegate(), // English fallback
],
supportedLocales: const [
Locale('en'),
Locale('es'),
Locale('pt'),
],
home: const HomePage(),
);
}
}Once registered, every layrz_ui component reading context.l10n picks up your translations
automatically — no per-component wiring required.
A custom delegate must be typed over the base LayrzUiL10n, never over a subclass:
// Correct — Localizations.of<LayrzUiL10n>() can find this
class MyDelegate extends LocalizationsDelegate<LayrzUiL10n> { ... }
// Wrong — silently never found; every component falls back to English
class MyDelegate extends LocalizationsDelegate<LayrzUiI18n> { ... }Flutter's Localizations.of<T>() looks a delegate up by its declared type parameter. layrz_ui
components ask for LayrzUiL10n specifically, so a delegate typed over LayrzUiI18n (a subclass)
is invisible to them — with no error, just silent fallback to English. LayrzUiI18nDelegate
follows this rule itself: it extends LayrzUiL10nDelegate but stays typed over the base
LayrzUiL10n.
The 133 keys are derived from LayrzUiL10n member names following a consistent dotted pattern
(actionCancel → 'actions.cancel'). You do not need to translate every key — any key missing from
your layrz_i18n engine falls back to the engine's own default behavior, and any key not overridden
by LayrzUiI18n at all inherits its English default from LayrzUiL10n directly. This also means
new strings added to layrz_ui in future releases keep working without requiring an update to this
adapter.
Eight duration-related keys (e.g. helpers.duration.days) use pluralized lookups via the engine's
tc() method, expecting a ' | '-separated singular/plural form:
'helpers.duration.days': '1 day | %count% days'
layrz_ui's LayrzAvatar component renders from a sealed LayrzAvatarSource hierarchy
(LayrzAvatarUrl, LayrzAvatarBase64, LayrzAvatarIcon, LayrzAvatarEmoji) that holds its data
directly, with zero knowledge of layrz_sdk. Apps that already hold an Avatar model from
layrz_sdk need to convert at the boundary — that conversion is what this package provides:
import 'package:layrz_sdk/layrz_sdk.dart';
import 'package:layrz_ui_extensions/layrz_ui_extensions.dart';
extension LayrzAvatarSourceConverterX on Avatar? {
LayrzAvatarSource? toLayrzUi();
}final sdkAvatar = Avatar(type: AvatarType.emoji, emoji: '🎉');
final source = sdkAvatar.toLayrzUi(); // LayrzAvatarEmoji('🎉')
LayrzAvatar(source: source);toLayrzUi() matches on the SDK Avatar's type and extracts the matching payload field
(url, base64, icon, emoji). It returns null — which LayrzAvatar treats as "no avatar,
fall back to initials" — for a null receiver, an explicit AvatarType.none, or any type/payload
mismatch (e.g. type is url but url is null or empty).
Why this lives here and not in layrz_ui: see decision
D36
in the engineering docs. layrz_ui briefly depended on layrz_sdk directly for this conversion
(D30), which forced a downgrade of an unrelated dependency (layrz_icons) to satisfy layrz_sdk's
own constraints — a design system inheriting a third party's transitive dependency problems. D36
removed layrz_sdk from layrz_ui entirely in favor of the native LayrzAvatarSource hierarchy,
and moved the SDK-facing conversion out to this adapter package, where a dependency on layrz_sdk
is expected and contained. See LayrzAvatar for the full LayrzAvatarSource
reference and its own migration note.
layrz_ui's own font system — LayrzFont, LayrzFontHandler, and the built-in
LayrzGoogleFontsHandler — fetches fonts from Google Fonts by default (Open Sans). This package
adds four ready-made LayrzFont constants sourced from the Layrz CDN instead, each a variable font
supporting a full wght axis:
| Constant | Family |
|---|---|
kLayrzFontInter |
Inter |
kLayrzFontNotoSans |
Noto Sans |
kLayrzFontOpenSans |
Open Sans |
kLayrzFontRoboto |
Roboto |
Each is backed by its own LayrzFont subclass (e.g. InterFont) that downloads its .ttf from
https://cdn.layrz.com/fonts/ via the http package and registers it with a FontLoader — this is
why the CDN fetch lives here rather than in layrz_ui: layrz_ui has no http dependency to make
the request with. Preload one the same way you would preload any LayrzFont:
import 'package:layrz_ui_extensions/layrz_ui_extensions.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await kLayrzFontInter.load();
} catch (e) {
debugPrint('Font preload failed: $e');
}
runApp(const MyApp());
}These constants are plain LayrzFont values — they compose with the rest of the font pipeline
described in Fonts (preload before runApp(), pass to LayrzThemeData.light() as
titleFont/bodyFont) exactly like a Google Fonts–sourced LayrzFont would.
layrz_ui ships its page transitions as bare LayrzTransitionBuilder functions
(LayrzPageTransitions.fade, .slide, .scale, .rotation, .none — see
LayrzPageTransition) rather than as a go_router-specific type,
for the same reason as everywhere else in this page: go_router is not a layrz_ui dependency.
Wiring one of those builders into a go_router route still means repeating the same
transitionsBuilder / transitionDuration boilerplate on every GoRoute.
LayrzTransitionPage<T> removes that boilerplate. It is a CustomTransitionPage<T> pre-wired with
a LayrzPageTransitions builder:
GoRoute(
path: '/detail',
pageBuilder: (context, state) => LayrzTransitionPage.slide(
key: state.pageKey,
child: const DetailPage(),
),
);Named constructors exist for each transition (.fade, .slide, .scale, .rotation, .none).
The unnamed constructor instead takes a type: LayrzTransitionType when the transition is a runtime
value rather than a fixed choice at the call site — for example, a single app-wide setting a user
can change:
GoRoute(
path: '/detail',
pageBuilder: (context, state) => LayrzTransitionPage(
type: settings.transitionType,
key: state.pageKey,
child: const DetailPage(),
),
);The generic T is preserved (not erased to Object), so a route pushed with
context.push<T>(...) and later popped with a typed result still type-checks through
LayrzTransitionPage<T>.
CustomTransitionPage.transitionDuration is a plain field read eagerly at construction — unlike
transitionsBuilder, it cannot be resolved lazily from a live BuildContext inside a callback. To
still honor the design system's token-based duration
(LayrzPageTransitions.durationOf, which needs a BuildContext under an installed LayrzTheme),
LayrzTransitionPage accepts an optional context parameter — pass the context argument your
go_router pageBuilder already receives:
pageBuilder: (context, state) => LayrzTransitionPage.fade(
context: context,
key: state.pageKey,
child: const DetailPage(),
),The precedence, highest priority first:
-
An explicit
transitionDurationargument wins outright, regardless ofcontextor the active theme — a caller who passes it is deliberately overriding the design system's default. - Otherwise, if
contextis given and aLayrzThemeancestor is found, the duration resolves toLayrzMotionTokens.dPageTransition(250ms by default) through the theme. - Otherwise —
contextomitted, or noLayrzThemeancestor found above it (e.g. a page built in a test with no app theme installed) — it falls back to the rawkPageTransitionDurationconstant, the same 250ms value the token resolves to by default, just read directly instead of through the theme.
reverseTransitionDuration defaults to the resolved transitionDuration, so pop transitions run at
the same speed as push transitions unless a caller deliberately asks for asymmetric timing.
For layrz_ui itself, see:
-
Getting Started — Set up
LayrzAppand preload fonts - Theming — Access design tokens in your widgets
- Fonts — The core font pipeline this package's CDN fonts plug into
- LayrzAvatar — The component this package's SDK conversion feeds
-
LayrzPageTransition — The transition builders this package wraps for
go_router -
Platform and Extensions —
layrz_ui's own extensions (a different package concern from this page)
Last updated: 2026-08-28
Made with ❤️ by Golden M, Inc.