Skip to main content

designless_serve 0.1.0 copy "designless_serve: ^0.1.0" to clipboard
designless_serve: ^0.1.0 copied to clipboard

Your brand in a Dart or Flutter app. Colours, type, spacing and marks come from your published brand, and change when you publish.

designless_serve #

Your brand in a Dart or Flutter app. Colours, type, spacing and marks come from your published brand, and change when you publish.

This package carries no brand data. It is a client for the addresses your brand is served at, plus the handful of rules a native platform needs followed before a downloaded typeface will actually appear on a screen.

It has no dependencies and does not import Flutter. HTTP and font registration are supplied by you, so the same core drives a Flutter app, a Dart server rendering email, or a test with no network at all.

Install #

dependencies:
  designless_serve: ^0.1.0

Use #

import 'package:designless_serve/designless_serve.dart';

final brand = Brand(
  publicId: 'r_XXXX',
  fetch: (url) async => (await http.get(Uri.parse(url))).body,
  store: myPrefsStore,          // optional: shows the brand on the first frame
);

await brand.init();

brand.tokens!.color('bg.page');                    // '#060608'
brand.tokens!.length('typography.fontSize.md');    // 14.56
brand.asset('logo-symbol', format: AssetFormat.png, size: AssetSize.px256);

store is worth passing. With one, the last payload is on screen before init() is awaited — the constructor restores it synchronously. Without one, the first frame is unbranded.

Fonts, and the mistake that costs the most #

A downloaded typeface does not appear because you downloaded it. It appears because the platform registered it and something asked for it by the right name. Get either wrong and nothing throws: the platform substitutes its own type, the screen looks plausible, and the bug is found in a screenshot weeks later.

Two rules follow from that, and this package holds you to both.

A face is reached by its PostScript name. Inter's 600 weight is Inter-SemiBold, not "Inter at weight 600". Core Text, Android's Typeface, SwiftUI, Compose and Flutter all resolve on the PostScript name. FontFace.postscriptName is the only string that reaches a face; family is a label for humans.

Register before you render. Text that has already been laid out does not go back and re-measure when a font arrives later, so a view built during the download keeps the platform's font for its whole life. FontStaging will not hand you a face whose file is not staged:

final staging = FontStaging(
  fetch: (url) async => (await http.get(Uri.parse(url))).bodyBytes,
  register: myPlatformFontLoader,   // must complete only once installed
  cache: myFileCache,               // optional
);

await staging.stage(await brand.loadFonts());   // download, cache, register

final body = staging.resolve('body', weight: 600);
if (body.usable) {
  useFontNamed(body.postscriptName!);
} else {
  debugPrint(body.detail!);          // says which of six things happened
}

Ask before staging finishes and you get FaceOutcome.notRegistered and a sentence explaining it — not a face that will not work.

What resolve tells you #

outcome what it means
resolved a face was found and the platform can reach it
substituted a face was found, but not the weight or style you asked for
notLoaded the font list has not been read — a fetch to retry
unpublished the brand publishes no face for this role, and the roles it does publish are named
tooFar the family publishes nothing within 200 of the weight asked for
notRegistered a face exists but its file is not staged — a build to fix

Substitution is reported rather than passed off as a match. A family publishing only Light, answering a request for Medium, looks exactly like a font that was applied.

Changes land when you say so #

A brand can change while your app is open. Restyling underneath someone mid-task is rarely what you want, and on several platforms it does not work anyway. So a fetched change is held until you activate it:

await brand.refresh();          // fetches, holds
brand.pending != null;          // something is waiting

// on next foreground:
brand.activate();               // now it is live, and `changes` fires

brand.changes fires on activation, not arrival — nothing a caller can see changed when the payload landed, and waking every listener to say so turns a stream into a rebuild storm. If you want the swap immediately, refresh(activateNow: true).

The first payload always activates. "Do not restyle underneath someone" needs something to be styled first.

Appearance #

brand.asset(...) uses the appearance of the payload that is live, not the one you asked for. Between asking for light and light arriving, those differ, and a light mark on a page still painted dark is the exact failure the rule exists to prevent.

Live updates #

EventStreamParser turns text/event-stream bytes into events. You own the socket; it owns the parsing.

final parser = EventStreamParser();
socket.listen((chunk) {
  for (final event in parser.add(chunk)) {
    if (event.hash != null && event.hash != knownHash) {
      brand.refresh();          // fetch now, activate on foreground
    }
  }
});

Keep-alive comments complete no event, so a client does not refetch the brand every time the server holds the connection open. parser.retryMs carries the interval the server asked for; honouring it keeps a brief outage from becoming a stampede.

Addresses #

Every address is built from the grammar the surface publishes, and parameters are only sent where that grammar says they apply. A few refusals happen here rather than as a 400 you would see as a broken image:

brand.asset('logo-symbol', size: AssetSize.px256);
// ArgumentError: an svg has no size to pick.

Sizes are a closed ladder — the surface renders at the sizes it has decided are legible and refuses the rest — so AssetSize is an enum you cannot express a failing request with. AssetSize.atLeast(200) rounds up to the next rung, because a mark drawn larger and scaled down stays sharp.

Composed destinations (app icons, social cards) take neither size nor appearance. The destination decides both.

Flutter #

Flutter bindings — ThemeData mapping, FontLoader registration, widgets — live in a separate package that depends on this one. This package stays pure Dart so it can be tested without a Flutter toolchain and used outside one.

Licence #

Apache-2.0

0
likes
150
points
72
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Your brand in a Dart or Flutter app. Colours, type, spacing and marks come from your published brand, and change when you publish.

Homepage
Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

More

Packages that depend on designless_serve