Skip to content

Extensions

Kenny Mochizuki Escalona edited this page Aug 28, 2026 · 1 revision

layrz_ui_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


Why this package exists

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.


Installation

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.3

The 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.


i18n binding

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 — implements LayrzUiL10n by mixing in 17 namespace mixins, one per LayrzUiL10n namespace, each mapping its members to dotted-key lookups (actions.cancel, calendar.monthNext, helpers.duration.days, …) against a LayrzI18n engine instance.
  • LayrzUiI18nDelegate — a LocalizationsDelegate that loads a LayrzUiI18n wrapping your engine.

Wiring example

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.

The delegate-typing rule

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.

Key coverage and partial translation

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'

SDK model conversions

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.


Font providers

layrz_ui's own font systemLayrzFont, 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.


Page transitions for go_router

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>.

Duration resolution

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:

  1. An explicit transitionDuration argument wins outright, regardless of context or the active theme — a caller who passes it is deliberately overriding the design system's default.
  2. Otherwise, if context is given and a LayrzTheme ancestor is found, the duration resolves to LayrzMotionTokens.dPageTransition (250ms by default) through the theme.
  3. Otherwise — context omitted, or no LayrzTheme ancestor found above it (e.g. a page built in a test with no app theme installed) — it falls back to the raw kPageTransitionDuration constant, 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.


Documentation

For layrz_ui itself, see:

  • Getting Started — Set up LayrzApp and 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 Extensionslayrz_ui's own extensions (a different package concern from this page)

Last updated: 2026-08-28

Clone this wiki locally