Skip to main content

hotlayout_flutter 0.19.1 copy "hotlayout_flutter: ^0.19.1" to clipboard
hotlayout_flutter: ^0.19.1 copied to clipboard

Flutter renderer for HotLayout server-driven UI. Draws screens published by the HotLayout builder from a JSON document, a theme and your data.

0.19.1 #

Documentation only: the README opens with what the package is, how to install it and the minimal example; the reference is grouped into tables by contract version. No code changes.

0.19.0 #

The first version published on pub.dev.

Reads document contract 9.19.0 completely (theme 4.0.1 and icon set 1.0.0 unchanged; data dictionary 1.5.0). This release executes the second half of the minor, on top of 0.18.0. Every row of the shared tables of 9.19.0 now runs.

  • Write actions and choice from plain nodes: setValue and toggleValue are performed by the reader through the write a field uses (the session of the screen stack and onWrite at the same moment, the typed value: String, bool, num), after the analytics event and never through onAction. The path and the value are literals of the document (paramBindings and carryRecord are not read); a number is parsed by the number field's own rule; the same value is not written twice (equality by type; an empty string equals an absent value); a toggle writes the negation of what is shown. An unusable action (WriteProblem) writes nothing. A path with a prototype segment or not of the form of a data path is refused by the write gate; the package has no writablePaths list of the host (fields have none either). A node whose action sets a non-empty value is a radio (inMutuallyExclusiveGroup, checked), a toggle a button with a toggled state; both for a button, an icon button, a list row with an action and a pressable card. The place in the group (choicePlaceOf) is a function of the model; this package does not speak it.
  • "Empty" is about the value (a named divergence of this reader): isEmpty / isNotEmpty judge the value under the path whatever its type. Before, a number and a flag were neither empty nor not empty and a string was trimmed first. The test visibility_test.dart "whitespace does not count as content" now says the opposite on purpose (a string of spaces is a value).
  • Parts of a list row: trailing.style / styleWhen, caption with captionStyle / captionStyleWhen, titleMaxLines / subtitleMaxLines, leading.color, leading.tile. The style of a part (PartStyle) is read field by field; a token the theme does not know is skipped, also in a state (the base field stays).
  • Parts of a text field: fieldRadius, fieldBackground, fieldBorderColor, fieldBorderWidth, fieldHeight, prefix, suffix (the first 8 code points of the shown text, or an icon), counter. The marks stand on the logical sides of the screen, never in the value, and a press on one focuses the field; the text of a mark and the counter are part of the description of the field.
  • alt is a string or a string of the document, translated by the key of the field; what is shown empty is decorative. iconPath: only the NAME of the set is taken from data; a name outside the set or a value that is not a string is an empty box. dateParts: DateFormat skeletons MMMMd, MMMMEEEEd, yMMMMd beside the date format only. unset (630–638): what a width layer removes goes before what it names, in styleOn and in propsOn (Screen.contentMaxWidth).
  • Parse limit: a screen whose tree goes deeper than kReaderParseLimit (32 levels below the root) is not drawn; a neutral notice (RendererLabels.screenUnavailable) stands in its place (depth.readerAboveParseLimit). 0.18.0 drew the first 32 levels. The reader of the web package rejects one level earlier (31 below the root); the artifact says 32.
  • Keyboard focus on a pressable card shows a ring in primary that follows the radius of the card.
  • Shared tables run by this release: writeCases (67), writeControlCases (22), writeSchemaCases (14), writeDocumentCases (47), writeTreeCases (11), writePressCases (15), choicePlaceCases (11), choiceControlCases (15), writeFormCases (6), emptinessCases (14), emptinessDocumentCases (10), list-tile-cases.json (91, eight groups), fieldBoxCases (16), affixRowCases (7), counterCases (16), noteRowCases (4), partsDocumentCases (20), iconPathCases (22), iconPathDocumentCases (9), datePartsCases (37), unsetStyleCases (14), unsetStateCases (3), unsetPropsCases (7), unsetDocumentCases (12), unsetScreenCases (5), and the row of nameCases with a description from data. Rows marked mayDiffer that Dart intl writes differently (U+202F before the year mark in Ukrainian and Kazakh) are named in template_cases_test.dart.
  • Fixtures choice-controls-screens, parts-screens and alt-icon-date-screens draw what they say.
  • ActionParamForm gained dataPath, literal and valueType, and the action catalog the two reader actions; the forms of the write parameters are judged by the write rule, not by paramFormProblem.

0.18.0 #

Reads document contract 9.19.0 (theme 4.0.1 and icon set 1.0.0 unchanged; data dictionary 1.5.0). This release executes the first half of the minor. The second half (choice controls and the write action, parts of a list row and of a field, the description of a picture, an icon and the parts of a date, "none" in a width layer) is NOT executed yet: their shared tables are copied and named in the tests, and their rows are not run.

  • Pressable containers: a Column, Row, Container or Stack with an action that does something (any type but none, or none with a track) is a button on the finished screen. The action runs through the same onAction as a button's; the innermost control under the point takes the press (a button, even one whose action is none; an icon button; a list row with an action; a field; a switch; a choice option; the tab strip; the header of an accordion), and the card gets everything else in its box without the margin. A disabled button swallows the press with an empty tap recognizer (not AbsorbPointer: an ancestor's recognizer would still win). One node of semantics: a button, the label first and then the text of the content; the controls inside stay separate nodes. Enter, Space; the auto-repeat of a held key is not a press. The card carries the record of its own list row.
  • Free composition: the reader never checked child types or depth and still does not; it now stops parsing a subtree 32 levels below the screen root (kReaderParseLimit, depth.readerParseLimit), so a hostile tree cannot overflow the stack.
  • The name of a card: the class of silent characters of x-accessibleName is the one of the artifact (kSilentCharactersPattern: whitespace, format and control characters, combining marks and five blank fillers). The text of a list from data and the own text of an Accordion or a Tab do not name a card (they are controls of their own or exist only with data); the Flutter semantics of an open accordion and the first tab give the rest.
  • Money as money: currencyDisplay, currencyPosition, sign on a binding (each read only on a format that reads it, an unknown word is no field); the composed form (number sign, currency before, amount, currency after), the signs U+2212 and U+002B written by the renderer, no sign on an amount written as zero, the currency signs from an embedded table of twelve codes, the gap U+00A0, and the isolation U+2066 … U+2069 around the composed form in a text written right to left. The direction is recovered from the strings of the document (LocalizedStrings.shownLanguage). A binding with none of the fields is written character for character as before.
  • Parts of an amount: a formatted number is up to five pieces with the part each belongs to (BoundString.pieces, formatBoundPieces); Text.spans[place].parts.fraction / .currency style them; a part with no style of its own merges with its neighbours, a declared part enters the line strut as place.part.
  • The top of the screen: Screen.props.bodyTop (one word, screenTop; anything else is no field) starts the body area at the top edge of the screen and lays the app bar over it (painted after the body, tested for touches before it); the whole box of the app bar takes a press, also where nothing is drawn. The reader adds the top safe inset from the MediaQuery.viewPadding of its own context: inside the app bar box (taller by the inset), inside the navigation bar's own strip, or above the body area when there is no app bar; none inside a modal layer, whose sheet is no taller than the area below the inset and whose window is centred in it. A non-number, NaN, infinity and a negative inset are zero (safeTopOf). A layer as tall as its content is not lower than the app bar that lies on it.
  • Data dictionary 1.5.0: the demo's sample data has account.* and the new survey.* paths.
  • Shared tables run by this release: composition-cases.json (all six groups), moneyCases (73), amountPartCases (19), amountPartStruts (5), bodyTopCases (7), topZoneCases (11), screenTopZoneCases (22, by formula and by real widget), screenTopPressCases (25, by formula and by the real hit test), screenTopRuleCases (27), screenTopDocumentCases (7), safeTopValueCases (10). The only row left out is the card named by a description from data («9.19.0 a description from data: its fallback is what is heard without data»), which waits for the description of a picture.
  • The table says 57 for the height of a navigation bar in two zone rows; the real bar of the built-in theme is 61, as in the bottom zone table, so those rows are checked by the formula the table vouches for, fed with 61.
  • Fixtures composition-screens, money-screens, hero-screens are in the catalog and the demo; choice-controls-screens, parts-screens and alt-icon-date-screens are there too and draw what 9.18 draws until their wave.

0.17.2 #

Reads document contract 9.18.0 with its clarifications (schema, theme, data dictionary and icon set unchanged in version). The only change in what a document draws is the rule of the spaces in shown text.

  • Spaces in shown text, exactly as the contract says (x-style.layout.shownText): runs of U+0020, U+0009, U+000A and U+000D are ONE space, the edges are trimmed, U+00A0 (and U+202F, and every other character) stays. 0.17.1 collapsed spaces and tabs only and kept the line feed, because shared tables were thought to build multi-line text with it; they do not (a line break is never in a string of the contract, a second paragraph is a second Text). Fields: the label, the placeholder, the helper and the error of a text field and the label of a switch or a checkbox are shown text and are collapsed too, as on the canvas (CSS white-space: normal; the only pre there are the value of a multi-line field and the label of a ring). The value of a field, the label of a bar and a ring, and everything that goes to the host are not touched. kShownTextRules mirrors the block of the artifact.
  • dateTime rows CLARIFIED-3 pass: date and time are joined by the CLDR pattern of the language (the rule was already in 0.17.1). On uk and ru Dart intl writes U+202F between the year and «р.» / «г.»; the contract now marks those two rows mayDiffer (either space is right), and the test names the platform text for them.
  • Placeholder, helper and error of a field are named in kShownTextRules.appliesTo (field-placeholders, field-helpers, field-errors; the label was already there). inputErrorOf returns the SHOWN text (spaces collapsed), as the reference does; the errorCases rows say what is shown.
  • Rows of the shared tables shownTextCases (26) and errorCases (250 in the inputs table) pass in the formula and in the real widget. The rows «no locale data» (tlh, dv, la) are checked strictly only where intl really has no data for the language (asked the way the reader asks, localeExists along the lookup chain); where a newer intl has it, the row (mayDiffer) only requires that the reader formats and does not fail.
  • Mirror of x-style.layout.widthClass.olderReader.showOnLeadingZone; modalContentAlwaysCompact, screenContentWidthClassIn, showOnAffectsOlderReaderOnCompact and showOnReplacesBackArrowForOlderReader in wide_chrome_rules.dart (the reader itself needs nothing: the class of a layer's content already comes from its box). The sections rows of wideSectionCases draw each screen by its own field and its own showOn.
  • Demo: the licence of Inter is the file that came with the font (Google Fonts download), not a text written by hand.

0.17.1 #

Reads document contract 9.18.0 (nothing in the contract changed). Fixes of what an independent review found looking at the Flutter reader beside the React reader and the builder canvas.

  • A page under a modal layer no longer jumps to the top. The stack of screens returned the bare frame without a layer and a Stack with one: the frame changed its place in the tree and was mounted again, so the page went to the start and stayed there after the layer closed. The Stack is always there now; scroll position, fields, focus and carousel memory survive opening and closing a layer.
  • Labels of the top navigation were cut with three dots (Arabic, 1280, web). The group measured natural label widths once, while the fallback font for Arabic arrives after the first frame: the items took the width of the fallback glyphs and the real text did not fit. RenderWideNavGroup now listens for system font changes and measures again; a label the rule counts as fitting is never clipped.
  • Esc closes the top modal layer (desktop and web), like a tap on the scrim. The system «back» is still the host's.
  • The label of a ring has tabular figures (FontFeature.tabularFigures()), as React's tabular-nums: «74 / 119» keeps its width as the digits change.
  • dateTime in English is «Oct 31, 2026, 2:00 PM»: date and time are joined by the pattern of the language (DATETIMEFORMATS of the same CLDR data), not by a space. The shared table template-cases.json has no row with a formatted dateTime (only the «shown as it is» ones), so CLDR and the React reader are the truth; the row is missing from the table.
  • Spaces in shown text (the builder canvas is the truth): runs of spaces and tabs are one space and the edges have none — in Text (literal, binding, template after substitution, and at the seam of rich-text pieces), labels of buttons and badges, list rows, the app bar and navigation items. Not touched: the label of a bar and a ring («3 / 4»), values and labels of input fields, what goes to the host. A line feed is kept for now: the shared tables (list items, long labels) build multi-line text with it. Three rows of the tables that expected the old behaviour (rich-text, template) are compared after the same rule until the contract takes it in.
  • No hover plate under a navigation item (web): Material's hover and splash are off for items of both bars, the keyboard focus mark stays. Material buttons keep their own hover (a different mechanism, named in the report).
  • No Material glyphs in the render path: the radio of a choice group is painted (ring and dot, the proportions of the Material glyph), and the placeholders of an asset, an image that did not load and the version notice use drawings of the icon set. A host without uses-material-design shows no squares; a test guards Icons. in lib/src.
  • data: images are drawn on every platform. Image.network reads them only on web (HttpClient has no data scheme), while the sample data of the dictionary is data: images. The reader decodes data:image/* from its bytes; an unreadable one is a broken image.
  • Demo (demo/): Inter as an asset of the demo (the theme names it), real images in the sample data, an empty survey form, launch parameters screen, locale, theme, a fixture named without themeUrl / documentUrl read from the assets, the log says where the theme came from. README: «Fonts» and «Demo».

0.17.0 #

Reads document contract 9.18.0 (theme 4.0.1, data dictionary 1.4.0 and icon set 1.0.0 unchanged). The wide place, step 2 of the adaptive layout. Step 1 (0.16.0) stays as it was.

Change in what a document draws. Nothing in the document changed, but the same document now looks different on a wide screen — that is the point of the step. On medium / expanded (from 600) the navigation bar is at the top (in the app bar, or its own bar when there is no app bar) and a modal screen without modalPlacement is a window in the centre, not a sheet. compact is pixel for pixel what it was. A host that draws a reader inside a wide area and expects the sheet at the bottom (a tablet app that shows the bar at the bottom of its own chrome) gets the new look; NavBar.widePlacement: 'bottom' in the document keeps the bar at the bottom on every width.

  • Where the bar stands (one place — screenChromeIn): compact → the bottom zone whatever the field says; widePlacement: 'bottom' → the bottom zone; otherwise the app bar when the screen has an app bar that is SHOWN in this class and by its condition, else a bar of its own. An app bar hidden by visibleIf / showOn is no app bar (the items would vanish with it). A bar that is not the last child stays an ordinary child of the body. The word of widePlacement is read tolerantly: a number, an object, null, another case, an unknown word are "no field" (top).
  • An item at the top: icon and label in a row, natural width (padding md, icon, gap xs, label of at most 160 with three dots, padding md), xs between items, no equal shares. Colour, the active item, tap and semantics are the rules of the bottom bar. Label bodyM.
  • Items in the app bar: part of the trailing zone, before the actions; the box of the bar (background, border, padding, shadow, radius, size) is not drawn, a colour named on the bar is inherited by the items. The rule of the app bar (title in the centre of the bar until it would touch a zone) is the same appBarZones — wideAppBarZones calls it.
  • A bar of its own: a zone of the app bar (fixed at the top, the body scrolls under it), edge to edge, root padding does not apply (the top padding stays with the body inside the scroll), surface background with a border line at the BOTTOM (a named style.background / border replaces them), items from the start of the row inside the column of contentMaxWidth.
  • When it does not fit (the order is part of the rule): the title shrinks to 120 (or to its own width), then ALL labels hide together (never one by one), then the title shrinks to zero, then the leading node, then the end of the bar is clipped; item icons and actions never shrink. The choice reads only NATURAL widths (a TextPainter without a width limit), so the view cannot flicker. A hidden label stays the accessible name of the item.
  • The zones of the screen (wideScreenZoneBoxes): bar in the app bar → no bottom zone unless there is a pinned block, and no safe inset without a bottom zone; own bar at the top → it takes its height and the rest is counted under it as for a screen without an app bar.
  • Sheet or window (modalShapeOf): decided by the class of the AREA the host gave the stack, not of the layer. A window of at most 560 (area minus xl on each side), as tall as its content but at most 85 % of the area height and at most the area without margins, centred on both axes; the body scrolls and the app bar of the layer stays; a pinned block is at the bottom of the window; no bottom safe inset. modalPlacement: 'center' is the window of 9.9.0 on every width (400). The class of the CONTENT of the layer comes from its own box (560 < 600, so compact). The root of a sheet shown as a window takes the radius of its bottom corners from the top ones when the style did not name them (sheetWindowRadius; a named corner — zero too — stays). Dismissing (scrim, system back, Esc), the scrim, the record and the shown event are unchanged.
  • Nothing is reset when a width crosses a limit: the stack, the session, the values and focus of fields, carousels and an open modal layer stay. The zones of the screen carry keys, so a bar moving between the bottom zone, the app bar and an own bar does not rebuild the body; the layer moving between sheet and window keeps its subtree (a GlobalKey). The keyboard lifts the window like the sheet: it never goes under it.
  • New: NavBarNode.widePlacement, NavWidePlacement, lib/src/model/wide_chrome_rules.dart (mirror of wide-chrome-rules.ts; the blocks x-style.layout.wideNav and wideModal are mirrored whole), WideNavGroup / RenderWideNavGroup (group of items, role navigation), AppBarLayout.hasNav. The two capabilities navBarOnWide and modalSheetOnWide are in the catalog (9.18.0).
  • The 151 new rows of adaptive-cases.json pass by formula; the geometry — by real widget in both writing directions (app bar 600 / 768 / 1024 / 1280 / 1920, bar of its own, the 24 combinations of the zones, the modal layer from 320 to 1920 with its content class). Fixture adaptive-screens is now four screens (home, lab, rewards without an app bar, bonus — a modal sheet); test/wide_chrome_cases_test.dart, test/wide_chrome_behaviour_test.dart, test/adaptive_fixture_test.dart.
  • Tests that quietly assumed the phone. flutter test draws on 800 × 600, which is medium: the sheet tests (screen_presentation_test, motion_test) now set a phone width, the contentCases rows (a bottom bar by definition) name widePlacement: 'bottom', and the real-flow row "a sheet on a tablet is still full width" of media-chrome-cases.json checks the window of wideModalCases (its formula modalBoxOf is unchanged).
  • Known and named: the side navigation (a rail) is not in the contract; inside the app bar the items do not scroll, what does not fit is clipped; a numeric inline padding above 128 on the bar (the row "200 of the bar's own inline padding") is clamped by the reader to the limit of the schema, so that row is checked by formula only.

0.16.0 #

Reads document contract 9.18.0 (theme 4.0.1, data dictionary 1.4.0 and icon set 1.0.0 unchanged). Adaptive layout by width, step 1.

Not in this release (step 2 of the contract). The navigation bar stays at the bottom and a modal layer keeps its sheet or window on every width; only the class of the box the layer is drawn in is measured. A document that relies on a side navigation or a wider modal waits for the next release.

  • The width class is measured with a LayoutBuilder over the root of the screen, in logical pixels, not rounded: compact below 600, medium from 600, expanded from 1024; an unbounded width is compact. It is the box of the screen, not MediaQuery.size: a widget in part of a tablet screen gets the class of that part. The limits are mirrored from the artifact (x-style.layout.widthClass).
  • The tree is resolved for the class before drawing: ScreenEntry.rootIn(widthClass) parses the raw root with the effective props (propsOn, a closed list per widget, columns 1…6 in an override, only perView of a carousel and only where the base has a carousel), the base style with the layers of styleOn (whole top-level fields, the way of a state) and showOn as a flag, then draws an ordinary document. Cached per screen entry and class; a document without showOn / styleOn / propsOn gets no copy. A change of the class does not reset the stack, the session, the carousels or the focus and value of a field.
  • showOn is the other half of "visible": ChildNode.isVisible is the class AND the visibleIf; one place for the zones of the screen, the actions of the app bar, the items of the navigation bar, the numbering of list items, the open tab, the form and required, stack layers, row weights, carousel cards and the pinned block.
  • Screen.contentMaxWidth: the body, the pinned block, the content of the app bar and the items of the navigation bar form a centred column (a symmetric inline inset of half the remainder, not rounded, the same in LTR and RTL); backgrounds stay edge to edge.
  • The 231 rows of adaptive-cases.json pass by formula and, for the geometry (weightCases, contentCases, gridCases, boxCases), by real widget in both writing directions on 393 / 768 / 1280 (and 599 / 600 / 1023 / 1024 for the limits). The reader has no validator: the 39 validityCases check that no document of the table makes it fail. Fixture adaptive-screens (en / uk / ar) is in the catalog and the demo.
  • Hostile shapes are tolerated as the reference says: an unknown class word is skipped (none known — the node is shown), a layer or propsOn / styleOn that is not an object is skipped, a key outside the list does not touch the base.

0.15.0 #

Reads document contract 9.17.0 (theme 4.0.1, data dictionary 1.4.0 and icon set 1.0.0 unchanged). Three minors in one release.

9.15.0 — alignment on both axes. Column.justify, Container.align / justify, Screen.align / justify. The cross axis is the existing rule of the column (align places only the children that do not stretch; there is no stretch word); the main axis acts only where there is free space (columnHeightSourceOf: style.height, style.minHeight or a stack layer with fill; for a screen the visible body minus what scrolls) and never stretches a column "to make it work". gap is the smallest distance (FlowGap), shifts are not rounded, between with one child or without free space is start; in a row container align is the vertical axis and style.minHeight gives no cross room. Screen.justify moves only the children of the body; Screen.align also places a pinned block of natural width. All 139 rows of align-cases.json pass by formula and, for boxCases and screenCases, by real widget in both directions.

9.16.0 — the app bar of a mock-up and an own glyph size. zone: 'leading' on a Button / IconButton child of AppBar (the first one in document order; the zone node and the actions are told by the FIELD, not by order or type), AppBar.titleAlign (start), glyphSize on Icon and IconButton. A visible zone node replaces the back arrow whatever showBack says, a hidden one leaves showBack in charge; it is at most half of the bar (the title shrinks first, actions never); the reader draws the first two VISIBLE actions. glyphSize is stronger than size and the typography, the system text scale does not multiply it, the box of an icon button grows with it and a named side clamps it. The 140 rows of 9.16.0 pass by formula and by real layout (rtlLeft included).

9.17.0 — text emphasis, list items, ring label. style.fontStyle / textDecoration (inherited per field, read by Text only; normal / none cancel), weight / fontStyle / textDecoration on a part of a composed text (a part with only emphasis is a part; the line is drawn by each run itself; the strut and the merging do not change), Text.marker (bullet, number: the column and the gap are fractions of the font size, the numbers count the visible neighbours, the column follows the group by the document, a numbered template of a Repeater counts the drawn records) and CircularProgress.labelTypography / labelColor (the label does not scale with the ring, does not inherit, is not clipped). 38 + 6 rows of emphasis / emphasisStruts, 69 rows of list-marker-cases.json and 22 ring-label rows pass.

Behaviour change. A visibleIf on the template child of a Repeater is now asked per record: an instance whose template is hidden is not drawn and takes no room (before, the condition was ignored there). A numbered template counts the drawn records.

Fixtures alignment-screens, app-bar-screens and text-emphasis-screens are in the catalog (test/fixture_screens_915_917_test.dart). Known and named: a numbered pinned block numbers itself (it stands in its own list); secondLineFrom of the list table is structural (the text is its own column), not measured; glyphSize of an icon button named smaller than its padding draws nothing but the padding overflows the box by the padding.

0.14.0 #

Reads document contract 9.14.0 and data dictionary 1.4.0 (theme 4.0.1 and icon set 1.0.0 unchanged). Form fields: required, disabled button, number field.

Change for host code. The value that reaches onWrite (SduiValueWrite, still void Function(String path, Object value)) can now also be a num: a TextField with valueType: 'number' writes an int for a whole value (12, never 12.0) or a double, and an empty String "" when the typed text is not a number or the field was cleared. Host code with an exhaustive switch over String and bool must handle num. A ChoiceGroup still writes a string, but it now also writes into the session of the reader (it did only to the host before) and does not write the same option twice.

  • required: true on TextField, Checkbox, ChoiceGroup (only the literal true): a red * after the label (one text with the label; not in the accessible name), Semantics.isRequired; no text of its own and no error state from emptiness.
  • A disabled button: requiresFilled: true and disabledIf on Button and IconButton. Half opacity of the whole widget, the role colours unchanged (disabledBackgroundColor / disabledForegroundColor equal the enabled ones), no tap / double tap / Enter / Space, no onAction / onTrack, stays in the focus order (a Focus of its own over the Material button, which would not take focus) and in the semantics tree with enabled: false. The form of a screen (isFormFilled, a mirror of form-rules.ts) is computed from the document and the data layers of the screen, lazily and once per build, so a button follows every keystroke without rebuilding the field (the controller and the focus stay).
  • A number field: valueType: 'number', own parsing (parseInputNumber, 65 rows of the table: no double.tryParse), stepNumberInput / shownNumberInput (numbers, not texts, are compared), one line, the numeric keyboard, content left to right.
  • A number field with a non-number is in the error state (a clarification before the 9.14.0 release). A valueType: 'number' field that is not focused and shows non-empty text that parseInputNumber does not read as a number (25 years, -, 1,2,3) draws the error look (the danger border, the same as a non-empty error) and reports the invalid state to the semantics tree; the reader adds no text (the helper stays). No state while focused; it is lifted when the shown text becomes a number or empty (only trimmed characters), and it looks at the SHOWN text, so a number from the data layers clears it. numberInputInvalid mirrors the reference; the group numberInvalidCases (11 rows) and four rows of formRuleCases pass by formula and by real widget.
  • The table inputs/input-cases.json now has 26 groups; the twelve new ones (300 rows, 68 form states) pass by formula and by real widget (form_cases_test.dart); fixture survey-screens (en / uk / ar, 320 and 393; survey_fixture_test.dart); sample data survey.* of the dictionary 1.4.0 in demo/.

0.13.0 #

Reads document contract 9.13.0 and data dictionary 1.3.0 (theme 4.0.1 and icon set 1.0.0 unchanged). Input fields: text, switch, checkbox.

Change for host code. The value that reaches onWrite (SduiValueWrite) is no longer always a string: a TextField writes a String, a Switch and a Checkbox write a bool (a ChoiceGroup still writes a string). A host that stored every value as text must be reviewed. renderDocument and ScreenFlow now take onWrite (before, only renderScreen did) and a new parameter autofill (default true; false gives no autofill hint to any text field — for documents from a source you do not fully trust). SduiValueWrite is now void Function(String path, Object value) (the value was Object?; it is never null): a handler declared with Object? still compiles.

  • Three widgets: TextField, Switch, Checkbox (28 widgets now). The value is a path of the dictionary ({ kind: 'binding', path }, the type of the path is the type of the field: a string for the text field, a boolean for the others); it is read through the data layers (record, session, host, data) and anything of another type is an empty field / off. Written on EVERY change, at once, to the session of the reader (conditions and texts on the same screen follow) and then to onWrite: no delay, no "on blur", no throttling in the reader. Not written: the same text again, a disabled field, a field without a path (it works locally), the intermediate text of an IME composition (written when it ends; TextEditingValue.composing), a value the platform filled in before the first touch (it is not kept either).
  • The held value is an EVENT, not a comparison at build time (syncTextInput, syncToggle): a focused field is the source of truth (the cursor does not jump when the host updates the data); an unfocused one keeps what was typed until the value of the layers CHANGES, then shows the layers. "A → B → A" is told apart from "nothing changed". TextEditingController and FocusNode live in the State of the field.
  • TextField: the label above the box (the accessible name), a placeholder drawn by the reader (a hintText would become the name), a helper, an error from the data (text that can be heard is the error state; it takes the place of the helper), keyboard (five intents to TextInputType + autofillHints from the data of the artifact; there is no password), maxLength in CODE POINTS (a formatter over runes, not the platform maxLength, which counts graphemes), lines 1…8 (anything else is one line, not the nearest bound), disabledIf (the evaluator of visibleIf; an unknown operator leaves the field enabled). The look is not Material: sizes and colours are data (46 dp box, md padding, sm radius, border 1; focus is a ring inside the box, so the size does not change; an error is stronger than focus). The content of an email, phone, address or number is always left to right; the alignment is from the start of the SCREEN (TextAlign.right on an RTL screen, not start). The keyboard button is "next" (focus to the next shown enabled text field of the screen, in document order) or "done" (focus off); it NEVER performs an action.
  • Switch / Checkbox are drawn by the reader (not Switch.adaptive / the Material checkbox): a track 52 × 32 at the END of the row, a box 20 × 20 at the START (logical, so mirrored in RTL), the tick is the check icon of the set at 16 and is not mirrored; the whole row is the touch area; roles toggled / checked; Space (and Enter on the switch) toggles ONCE — a held key (auto-repeat) is not a press (KeyRepeatEvent is eaten).
  • The keyboard (bottomZone.keyboard of 9.12, now in force): the reader reads MediaQuery.viewInsetsOf of its OWN context and leaves the Scaffold of the host alone. The body ends above the keyboard (above the pinned block when there is one), the focused field with its helper is scrolled into it, the pinned block rides above the keyboard, the navigation bar stays under it, the safe inset is not added above it; a modal layer rises above the keyboard. A host that already resized the screen gives zero in the context of the reader, so nothing is subtracted twice. ScreenZones now has FOUR children (hasBar, hasPinned, hasLow: the pinned block and the "bottom" with the bar are measured apart) and a keyboard parameter.
  • Tap outside a field removes focus; a move between screens removes focus; Tab visits the switches and checkboxes too.
  • Shared table fixtures/inputs/input-cases.json (248 rows, 14 groups) is read by test/input_cases_test.dart: every row by the reference formula (lib/src/model/input_rules.dart) and by the real widget (tester.enterText, taps, focus, sequences of writes). test/inputs_behaviour_test.dart — the flow with the host, focus, disabledIf, autofill, the keyboard, semantics. inputs-screens is in the demo catalogue (the demo writes the fields to the log and sends nothing) and test/inputs_fixture_test.dart (en / uk / ar on 320 and 393).
  • Where the contract is silent or contradicts itself: the contract text says a switch value in an action parameter is the string true / false, while x-actions.paramBinding (9.8.0) accepts only a non-empty string — the reader follows the artifact (a boolean is not a parameter; the literal of params acts). The narrow-place rows of geometryCases are not drawn by the contract (a row narrower than the control): the switch and the checkbox hide the label and clip, and do not overflow.
  • kSupportedSchemaVersion is 9.13.0; the demo mirror of the dictionary has the eight new paths (form.*, settings.*). A 9.13.0 document read by 0.12.0: the three widgets are unknown (nothing is drawn).

0.12.0 #

Reads document contract 9.12.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged). Bottom zone of the screen: navigation bar and pinned block.

  • The bottom zone of a screen. The children of the screen root are shared among the zones by ONE function (screenChildrenByZone, lib/src/model/bottom_zone_rules.dart), by the document and not by visibility: the app bar is the first child when it is an AppBar; the navigation bar is the LAST child when it is a NavBar (otherwise an ordinary child of the body); the pinned block is the last child not counting the bar when its pin is bottom (anywhere else the field is not read). A node hidden by a condition has no zone, and nobody takes its place. The zone stands at the bottom and does not scroll; the body ends at its TOP edge and is clipped there; the root paddings are shared (the block gets the side and bottom ones, the body gets the screen gap instead of the bottom padding); pull on the pinned node is not read. ScreenZones got the zone as its third child (one render object for three zones, not a wrapper).
  • NavBar / NavItem (25 widgets now). Visible items share the width equally, no gaps, no rounding; more than five visible: the first five are drawn. Icon above the label (md icon box, bodyS label on one line with an ellipsis), paddings sm / xs, height 61 in the default theme. Built-in look: surface background, a top line of 1 in border; the node's style.background / style.border REPLACE them. Active item primary, the rest muted; a named colour keeps the active item as is and fades the rest. The touch area is the whole share at the full height. Semantics: the bar is a navigation landmark (SemanticsRole.navigation, only the bar of the bottom zone), an item is a button named by its label (name from the icon name when the label is empty), the active one is selected.
  • navigate.section (x-actions.section): the stack becomes the ONE named screen (everything above and the modal layers are closed, the memory of carousels of the dropped frames goes too); the same screen: nothing and no event; otherwise one onScreenShown event with cause: switch (ScreenShownCause.switchSection), depth 1, no record, page even when the section is declared modal. An unknown screen goes to the host like an unknown navigate; the analytics event goes first. navigate is unchanged.
  • The bottom safe inset is added by the reader UNDER the content of the bottom zone and only when the zone exists: MediaQuery.viewPaddingOf(context).bottom. The background of the bar reaches the physical bottom; under a block without a bar it is transparent room under the root bottom padding; a modal sheet has it, a window in the middle does not. A host that wrapped the reader in SafeArea (or MediaQuery.removePadding) gives zero — nothing is doubled. No parameter of the host is needed.
  • The keyboard rule (bottomZone.keyboard) is only mirrored: there are no text fields in 9.12.
  • Shared table fixtures/bottom-zone/bottom-zone-cases.json (131 rows) is read by test/bottom_zone_cases_test.dart (every group; zones and the bar by the formula and by the real widget), the eleven 9.12 sections: rows of action-cases.json by test/actions_cases_test.dart. bottom-zone-screens is in the demo catalogue and test/bottom_zone_fixture_test.dart (en / uk / ar on 320 and 393).
  • kSupportedSchemaVersion is 9.12.0. A 9.12.0 document read by 0.11.0: pin is not read (the block scrolls with the body), NavBar is an unknown widget (nothing is drawn), navigate.section goes to the host.

0.11.0 #

Reads document contract 9.11.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Carousel (props.carousel on a Row with scrollable: true and on a Repeater with direction: 'row' and no columns; anywhere else the field is skipped). The sideways scroll snaps to the START of every card, with dots under it. A Repeater with the field scrolls by itself (the only place the field turns scrolling on). perView (1…6) gives every card the width (viewport - paddingInlineStart - perView * gap - peek) / perView, not rounded; it wins over the card's own width / minWidth / maxWidth (the weighted slot drops them; the card's margin lies on top); a card narrower than 1 means perView is not read. peek is a spacing reference (default lg).
  • Snapping is a ScrollPhysics (CarouselPhysics, lib/src/render/carousel.dart) over the list of stops carouselStops (card starts, the last stop is the end of the scroll, stops within 0.5 of it merge); the stops are measured from the real layout. PageView with viewportFraction was not used: it centres the page and cannot stop the last card at the end of the scroll. Dots: one per STOP, none for one stop or more than ten (and then they take no room), not a control, excluded from semantics, inactive = the active colour at 0.3. The state (which stop) is the reader's: it lives in the scroll controller of the widget. The list gets SemanticsRole.list, the cards listItem.
  • Stepped progress (ProgressBar.segments 2…12, currentColor): the step is filled whole or not at all (floor(round(ratio * N, 6))), the current one is the first unfilled, no track under the gaps (gap xs), the gradient runs along the filled steps with the gaps cut out (one shader, one rectangle per filled step), a step narrower than 1 or a wrong segments means the continuous bar. One semantics node, as for the continuous bar.
  • Shared tables fixtures/carousel/carousel-cases.json (68 rows) and the three new groups of fixtures/progress/progress-cases.json (30 rows) are read by test/carousel_cases_test.dart and test/progress_cases_test.dart: every row by the reference formula (lib/src/model/carousel_rules.dart, progress_rules.dart) and by the real widget (gestures by drag / fling, both directions). carousel-steps-screens is in the demo catalogue and test/carousel_fixture_test.dart.
  • A row of dots that does not fit the area is not drawn and takes no room (ten dots of sm with a gap sm are 152 dp, and on a narrower area the row used to stick out): the same rule as «more than 10 stops». carouselDotCount takes the room.
  • The rule of the dots is ONE function, carouselDotCount(viewport, dotSize, dotGap) with carouselDotsRowWidth (indicator.dotsRowWidth, dotsRowWiderThanViewport: 'not-drawn'); the widget only asks it, and the shared table has 10 dotCases rows, each with its viewport.
  • Stack: «a sizing layer is left» counts the DOCUMENT, not visibility: an overlay (layer) stays an overlay when the sizing layer is hidden by a condition, and the stack is of zero size instead of the overlay jumping from the corner into the flow.
  • A column with a pulled first child answers hit tests for the strip above itself, so the overlap over the bar takes taps.
  • Clarifications before the release:
    • The state of a carousel (which stop is open) belongs to the STACK FRAME (CarouselMemory) and lives as long as the frame is in the stack, even while the screen under the top one is not built; the key is the node plus the chain of template-instance indices (carouselStateKey), so each row of a list from data keeps its own card after «back». It used to live in the widget State and was lost.
    • The first screen of a flow is always a page: ONE place (_presentationAt) answers the effective presentation for both drawing and the shown event, so a screen named modal but shown first is drawn as a page (no shrink) and the event says page without modalPlacement.
    • The app bar draws at most the first two VISIBLE actions (appBarExtraActions: 'draw-first-two'); with eight actions the title used to get a width of 0.
    • pull on an app bar is not read (the bar is a zone of the screen, not a child of the flow).
    • A stepped bar has the same own minimum width, (N - 1) * gap, in both kinds; the kind is decided by the width the place gives. In a row of natural width it is 44 and continuous, stable between frames.
    • Tables: segmentPlaceCases (6), areaCases (4) and stateCases (6), modalRuleCases (2), appBarDrawnCases (7), more rows of shownCases, pullReadCases, pullDocumentCases and ringCases; the mirrors of the artifact keys (progressSegments, carousel.area/shell/active, modalLayer.above, appBar.maxDrawnActions, pull.onAppBar, the policy row appBarExtraActions).
  • kSupportedSchemaVersion is 9.11.0. A 9.11.0 document read by 0.10.0: the row scrolls without snapping and dots, a list in a row does not scroll, the bar is continuous.

0.10.0 #

Reads document contract 9.10.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Overlay layers of a stack (node.layer): a direct child of Stack with layer is an overlay. It is placed against the box of the stack with its own alignment (else the stack's), shifted along the axes of the stack (x logical, mirrored in RTL; y physical; a spacing token or an integer 0…128), then turned about its centre by a whole number of degrees clockwise (the sign is mirrored in RTL). An overlay never changes the size of the stack: the stack is measured by the layers without layer and fill; if there is none, the field is read on no layer. Its width is at most the stack's, its height natural. Drawn by LayerPlace (lib/src/render/layer_place.dart), a render object placed like Positioned.fill: the shift is LAYOUT, so a tap lands where the layer is drawn (inside the stack box), not where it stood. Transform.translate was not used for this reason.
  • The stack no longer clips (clipBehavior: Clip.none; it used to be hardEdge). The radius, a named size, scrolling and the screen clip as before.
  • Pulled children of a flow (node.pull): in a child of Screen, Column, Container and a Row without wrap and scrollable the node is pulled towards the start of the main axis by a spacing token or an integer; the following siblings move with it, the parent gets shorter (not below zero), the gap stays (overlap = pull - gap), the first child is pulled too, a later child lies over an earlier one and catches taps in the overlap. A weight wins over pull; Repeater and the other parents do not read it. Done inside the layout of RenderStretchColumn / RenderRowLayout (a pull in the parent data, FlowPull / RowSlot.pull), not by Transform (it moves the drawing but not the hit area) and not by a wrapper "size - pull" (that breaks when pull is bigger than the child).
  • Shared table fixtures/offset/offset-cases.json (97 rows in 7 groups) is read by test/offset_cases_test.dart: rectangles, angles and tap points through the reference formula (lib/src/model/offset_rules.dart) and through a real document in both directions (taps by the real hit test); the flow arithmetic through a real column and a real row. offset-screens is in the demo catalogue and test/offset_fixture_test.dart.
  • kSupportedSchemaVersion is 9.10.0 (versions compare numerically, so 9.10.0 is newer than 9.9.0). A 9.10.0 document read by 0.9.0: layers are ordinary layers, pull is ignored, and a stack clips what sticks out of it.

0.9.0 #

Reads document contract 9.9.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Own proportion of an image (Image.aspectRatio): a word (1:1, 16:9, 4:3) or a pair { width, height }; the ratio is clamped to 1:8 … 8:1 and one the reader does not understand is 16:9.
  • The box of an image is the box of its node. The named style.height wins over the proportion (a 56 × 56 avatar stays 56 × 56); the proportion only gives the height from the width; minHeight / maxHeight clamp the height by the proportion and the named one alike, the minimum winning; the picture lies in the box by fit, centred, and is clipped by the node's radius. The natural size of the file is never read: the box is the same before, during and after loading. New ImageFrame instead of AspectRatio (which shrank the WIDTH under a limited height).
  • App bar actions: the children of AppBar of type IconButton (any number, in document order) are drawn in the trailing zone as ordinary icon buttons; other children are not drawn. Three zones, a gap only next to a zone with content, hidden-by-condition actions take no room, right-to-left keeps the order of reading. New AppBarLayout.
  • Changed behaviour: no counterweight opposite the back arrow (appBarTitleRoom). The title is centred in the BAR while it fits and shifted just off the zones otherwise; a title longer than "bar minus two arrows" used to be cut there, now it takes the room between the zones.
  • The app bar is fixed; only the body of the screen scrolls (appBarFixed, appBarScroll). The bar is the first child of the screen root when it is an AppBar (by the document: hidden by a condition it has no zone, and the next node does not become the bar), stands at the top inside the top and side padding, and the body scrolls under it with the side and bottom padding and the gap of the screen instead of the top padding inside the scroll; the background of the root lies under every zone and does not scroll; the body area ends at the bottom edge of the bar and clips there. A pulled first body child raises the body area by its overlap (pull minus the gap, not above the top of the screen) from the first frame, no scroll listener. A modal layer has the same zones: its body scrolls, its bar stays. New ScreenZones (lib/src/render/screen_zones.dart), reference screenZoneBoxes / screenAppBarIndex.
  • Modal window: ScreenEntry.modalPlacement (bottom | center) of a modal screen. A window is the area minus xl on each side, at most 400, centred; taller content scrolls inside it. No field or an unknown word is a sheet from the bottom. A page never reads the field; neither does the first shown screen.
  • No built-in look of a modal layer: background, radius, shadow and padding are the style of the screen's root. The scrim is black at 0.45; a tap beside the layer, the system back button and a button navigate.back all take one step back.
  • Status bar icons: ScreenEntry.statusBarIcons (light | dark) is a REQUEST. The package touches nothing (no SystemChrome or AnnotatedRegion in lib/, a test guards it); the request goes to the host as ScreenShown.statusBarIcons, and ScreenShown.modalPlacement says where a layer lies — both only when the screen named them. demo/ carries a request out with AnnotatedRegion as an example; the README shows the code.
  • Shared table fixtures/media-chrome/media-chrome-cases.json (144 rows in 12 groups) is read by test/media_chrome_cases_test.dart: each row twice, by the reference formula (lib/src/model/media_chrome_rules.dart) and by the real widget (image box in three views, the app bar layout in both directions, the modal layer by a real flow, the event key for key); the two flows of record-scope-cases.json with a window run in record_scope_test.dart. media-chrome-screens is in the demo catalogue.
  • kSupportedSchemaVersion is 9.9.0. A 9.9.0 document read by 0.8.0: no actions in the bar, a window is drawn as a sheet, an own proportion is 16:9, the request for status bar icons is not passed on.

0.8.0 #

Reads document contract 9.8.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Action catalog (schema 9.8.0): none, navigate, navigate.back are performed by the renderer; openUrl, deeplink, close by the HOST. They reach onAction / onRecordAction parsed and checked: ButtonAction.standard (OpenUrlAction, DeeplinkAction, CloseAction) when every parameter passed its form, ButtonAction.invalid (InvalidActionParam: param, problem) when one did not. Never both; an app-specific action has neither. An invalid action is still handed over.
    • The renderer opens nothing by itself and the package has no new dependency. runStandardAction(action, openUrl:, deeplinkSchemes:, openDeeplink:, close:) is an optional helper the host calls with its own executors; it acts only on standard, and a deeplink only when its scheme is in the host's deeplinkSchemes.
    • The form of an address is a regular expression of the artifact (x-actions.forms), mirrored word for word and checked against the artifact by a test — not Uri.parse. Order of checks: missing, too long, form.
    • paramBindings: a parameter read from data with the data scope of the node at the moment of the tap. Only a non-empty string (up to 2048) replaces the literal; a number is not turned into text; a value that fails the form is not replaced by the literal.
    • close is the host's; the stack is not touched, on the first screen or the fifth, on a modal sheet too.
    • The limit of an action parameter value is 2048 characters (was 128).
  • Analytics event of a press (action.track): onTrack(TrackEvent, nodeId, record) once per tap and BEFORE anything else — before a navigation, before the handed action, for none too. Not part of the handed action. A track of the wrong form is ignored whole, the action works.
  • "Screen shown" event: onScreenShown(ScreenShown) with documentId, screenId, entry, presentation, cause (entry / forward / back), depth and record. Exactly one per change of the screen on top of the stack (the system back button and a closed sheet included), after the frame. onScreenChanged is deprecated and called at the same moment.
  • carryRecord: true on navigate: the page opens with the record of the tapped node. The default rule of 9.2.0 is unchanged.
  • none no longer reaches the host. It is a reader action that does nothing (the contract says so); it used to be handed over like an unknown action.
  • A re-parsed copy of the same document is the same flow. ScreenFlow used to reset the stack whenever the UiDocument object changed; a host that parses JSON in build was sent back to the first screen on every rebuild. Now the stack is kept when the documentId is the same, the entry is the same and every screen of the stack still exists (the screens are taken from the new object); otherwise the flow starts again and shows the first screen with cause entry.
  • Shared table fixtures/actions/action-cases.json (form 108, handed 55, flow 28 rows, and the schema/document tables as "does not crash") is read by test/actions_cases_test.dart; the fixture actions-screens is in the demo catalogue, where the standard actions only write to the log and a SnackBar.
  • Clarifications of 9.8.0 before the release (the version stays 0.8.0, it is not out):
    • OpenUrlAction.url and DeeplinkAction.url are the checked STRING, byte for byte (the old source). The Uri field is gone; Uri? get uri is a convenience (null when the platform cannot parse the address, or when reading its port would throw — the verdict never changes). The verdict standard / invalid comes from the form expression alone: the Uri.tryParse … == null branch is removed, so the same six addresses are no longer standard in React and invalid here. runStandardAction hands the executors a string: void Function(String url)? openUrl / openDeeplink (breaking against the unreleased 0.8.0 draft only).
    • The https form is narrower (word for word from the artifact): a host of two or more labels whose last label is letters (two or more) or xn--…, so no IPv4 in any notation and no single-word name; a port is 1…65535 without leading zeros.
    • A binding to a parameter the catalog marks bindable: false (navigate.screenId) is ignored: the literal acts, in the flow and in the action handed to the host. Another action and a parameter the catalog does not name read data as before.
    • params entries: skipped when the key is empty, has whitespace, is longer than 32 characters or is __proto__ / constructor / prototype, or when the value is not a string (a number or a flag is NOT turned into text any more — it was in 0.7.0) or is longer than 2048 characters. The same key rule for paramBindings; a track whose event name or parameter key is constructor / prototype is ignored whole.
    • A held Enter / Space is ONE press. The auto-repeat of the key (KeyRepeatEvent) used to reach onPressed of every Material button (a held key = one press plus a press per repeat: an action and an analytics event each time); it is now swallowed before it reaches the button's shortcuts. A list row has no keyboard activation, so there is nothing to repeat there. A double tap is two presses; protecting a payment from it is the host's job.
    • runStandardAction takes type, url and scheme once, re-checks the form over exactly those values and hands the executor exactly them; deeplinkSchemes must be a collection of strings, anything else means "no schemes".
    • The table grew to form 108, schema 38, document 27, handed 55, flow 28 rows; the runner knows the steps newVersion and press with repeat: true (a real keyboard on a focused button). Four policy rows: unbindableParamBinding, keyAutoRepeat, stackScreenGone (a new version of the document without a screen of the stack starts the flow again from the entry, which ScreenFlow already did), actionParamComposition.
    • README: what stays with the host — open only standard, never the raw params['url'] of an invalid action, an older package hands 9.8 openUrl over unchecked, the content of a deeplink is untrusted, a double tap is two actions, do not build a route from params['screenId'].
  • kSupportedSchemaVersion is 9.8.0. A 9.8.0 document read by 0.7.0 hands openUrl, deeplink and close over as unknown actions, unchecked (raw params, no verdict), and sends no analytics or screen events.

0.7.0 #

Reads document contract 9.7.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Progress look (schema 9.7.0): ProgressBar and CircularProgress read thickness (a spacing token or a whole number 0–128), trackColor and gradient. All three are optional and read one by one: a wrong form of one does not cancel the rest. Without them the scale is what it was — sm thick, a secondary track, the solid color.
    • The track lies under the whole scale, the fill over it, so a translucent fill shows the track beneath. The fill is the gradient when it resolves, else color; a gradient that does not resolve (unknown token, stops going backwards, a stop on an unknown colour) is dropped as a whole.
    • A gradient lies along the FILLED part: position 1 is the end of the fill, not the end of the scale, so a brand transition is fully visible at any value. Its angle is not read. On a bar the line runs from the logical start of the fill (the right in a right-to-left language); on a ring it is a sweep gradient from the top, clockwise.
    • Zero thickness draws neither track nor fill; the box stays.
  • Ring diameter from the node's box: style.width / height and their min… / max… size the RING (the smaller named side of the content box, else the size word). A size given by the parent — a row weight, a stack fill layer — names nothing; the ring keeps its size and stands at the centre of the given place instead of stretching into an oval.
  • Changed look (the four places where 0.6.0 differed from the contract):
    • the arc lies INSIDE the circle — a md ring is 72 dp across, not 72 + thickness (Material put the stroke on the edge: a ring of 48 was 56 wide);
    • a bar with a limit of 0 or less is empty, not full;
    • the fill of a bar has a rounded end (radius pill, clamped to half of the shorter side), not a flat cut;
    • the arc starts at the top, runs clockwise and is not mirrored in a right-to-left language (it was already so, now it is a rule).
  • The shared table fixtures/progress/progress-cases.json (84 rows: ratio, ring geometry, arc, bar geometry, paint) is read by test/progress_cases_test.dart; the fixture progress-screens is in the demo catalogue.
  • Fixed: a Stack in a Row took the whole row (an infinite-width SizedBox around it), so a card grew to the screen height and the column beside it vanished. A stack is now sized by its non-stretched layers; only a stretching parent (column, row share with weight, grid cell) gives it the full width.
  • kSupportedSchemaVersion is 9.7.0. A 9.7.0 document read by 0.6.0 draws a solid scale of the old geometry — the new fields are skipped.

0.6.0 #

Reads document contract 9.6.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Rich text (schema 9.6.0): Text.spans styles parts of one string — a big number next to a small caption (1,850 Points), a bold word in a sentence. It is still a Text; there is no new widget. spans maps the name of a place of the content template to a style (color, typography); the style follows the name, so a translation may reorder the parts (Балів: 1 850) and each part keeps its style.
    • The text is exactly the text without spans: no character is added, every rule of the template (formats, plurals, fallbacks) is unchanged. A fallback, a formatted value (120 JOD) and a plural word take the style of their own place.
    • One paragraph: words wrap across the parts, the parts share one baseline, align, maxLines and overflow work as for a text without parts, and the bidirectional order is that of the whole string (a part is never isolated). A screen reader hears one node with the whole string.
    • One line pitch for the whole paragraph: the tallest line (font size × line height) among the text style and every part the node declares, whether or not the shown string uses it; its style is the strut (StrutStyle with forceStrutHeight). Ties: the larger font size, then the text style, then the part name in code-unit order.
    • A part style is laid over the text style by the inheritance rule: a named field replaces the field of the text as a whole; a token the theme does not know leaves the text's own value.
    • Tolerant reading: spans that is not an object, a style that is not an object or has no readable field, an unknown field and a field of the wrong form never throw; a style for a name the template does not declare, or on a text that is not a template, does nothing.
  • A word in a template: the value {kind: 'text', text} — a named word of the document, translated by <place>.values.<name>.text; without a translation the text of the document is shown and the rest of the sentence stays translated. Works in every template, not only in Text.
  • kSupportedSchemaVersion is 9.6.0. A 9.6.0 document read by 0.5.0 shows the string in one style and an empty place where a word stood.

0.5.0 #

Reads document contract 9.5.0 (theme 4.0.1, data dictionary 1.2.0 and icon set 1.0.0 unchanged).

  • Button sizes (schema 9.5.0): Button.size is sm, md or lg. Padding and label font come from the contract's size table in theme tokens — 28 / 40 / 48 dp high with the built-in theme. A button without the field is md, exactly the button of 0.4.0.
  • An icon in a button and in a badge: icon and iconPosition (start / end, in reading order — the sides swap in a right-to-left language, and directional icons are also mirrored). One rule for the button label, the badge label and the trailing text of a list row: a 4 dp gap, the icon centred on the line of the text, in the colour of the text, sized by the font of the text. The icon does not change the height of the widget.
  • New widget IconButton: a round button with an icon and no visible text, in the three button sizes (as high as a button of the same size) and the theme's button roles. Its label is the accessible name — translated, bound to data, announced with the role of a button, never drawn; without a label the button is named by its icon. Its action goes the same way as a button's.
  • Changed look: the trailing text of a list row and the icon beside it are now onSurface (the colour of the row's title) instead of muted. The contract was silent here and readers disagreed; it now names the colour (listTileTrailingColor). A named style.color still wins; the subtitle and the chevron stay muted.
  • Changed look, non-Latin scripts: the label line of a button and of a badge is exactly the line height of its font. A fallback font (Arabic, for one) used to make the line, and with it the button, a few dp taller than the size table says.
  • The touch area of the new small button and of the icon button is extended to 48 × 48 dp around them; their visible size and the layout are not.
  • An icon that cannot be drawn leaves an empty box and keeps the gap; an unknown size is md, an unknown iconPosition is start; none of this throws.
  • Clarifications of 9.5.0 before release (five policy rows):
    • A date must be a real calendar moment. After the shape check, the groups are checked by the reader: month 1–12, day within the month of that year (Gregorian leap rule), hour 0–23, minute and second 0–59, offset hours 0–23 and minutes 0–59. 2026-02-30T…, month 13 and hour 24 are not dates and are shown as they are — for date, dateTime, time and daysUntil alike (Dart used to read month 13 as January and 30 February as 2 March).
    • Rounding is half away from zero over the shortest decimal form of the number, not binary arithmetic: 1.005 is 1.01 USD, 1.0005 is 1.001, 2.675 is 2.68. Group and decimal separators and digits still come from the language.
    • Plural of a fraction follows CLDR operands: French 1.5 and Hindi 0.5 are one, English 1.5 is other. Intl.pluralLogic computes the integer part as n.round(), so the renderer carries the CLDR rules of intl (generated, tool/generate_plural_rules.py) and passes the operands itself; a test compares them with intl on integers for every language.
    • The magnitude limit is symmetric: -9007199254740994 and -9223372036854775808 are shown as they are under number and currency.
    • A button-icon label of only whitespace or invisible characters (U+00A0, U+FEFF, U+200B, … — class [\s\p{Cf}]) is empty: the button is named by its icon.
    • An icon body that fails to parse for any reason (not only FormatException), or has a negative radius or size, draws as an empty box.
    • A button label is centred, the second line included (buttonLabelAlign: 'center').
  • kSupportedSchemaVersion is 9.5.0. A 9.5.0 document read by 0.4.0 shows buttons and badges without icons at the default size, and nothing in place of an IconButton.

0.4.0 #

Reads document contract 9.4.0 (theme contract unchanged, 4.0.1; icon set unchanged, 1.0.0) and data dictionary 1.2.0.

  • Sentences with places (schema 9.4.0). A text can be kind: 'template': a translatable string with named places ({done} / {total} steps) and a map of values. The places are substituted in one pass; a place whose name the document does not declare stays as plain text with its braces; a value with no data shows its own fallback and the rest of the sentence stays (— / 5 steps). The sentence and the plural forms are translated by the keys <place>.template and <place>.values.<name>.forms.<category>. Works wherever the contract has a text for people: Text, Button, Badge, the titles and subtitles of AppBar, ListTile, Tab and Accordion, the trailing text of a row, and inside a list drawn from data.
  • Number formats: format: 'number' (group separators of the language, always on, up to three fraction digits), format: 'currency' with currency: 'JOD' (the amount, a no-break space, the code — 120 JOD, one order in every language; a whole amount has no fraction) and format: 'daysUntil' (whole days until the moment, rounded up, never below zero, no time zone). They follow the formatting locale of the document, as dates do.
  • Plural words: a template value kind: 'plural' shows the form for the CLDR plural category of a number, in the language of the shown forms. zero, one and two are categories, not the numbers 0, 1 and 2: English 0 is other. A missing form, a missing number and a document without localization all give other.
  • New: the now parameter of renderDocument, renderScreen and ScreenFlow — the moment "days until" is counted from. Without it the device clock is read when the screen is built. The screen does not redraw itself as time passes.
  • New: LocalizedStrings.sourceLocale — the source language of the document the table came from; UiDocument.stringsFor fills it. Plural words of an untranslated sentence follow its rules.
  • Changed behaviour: a binding whose data is a map or a list now shows its fallback text (it used to print the Dart value), and so does a binding whose data is an empty string (it used to show nothing) — ChoiceGroup.value is not affected. A whole number that arrives as a very large double is no longer clipped to the 64-bit range when shown as it is.
  • Changed behaviour: a date is a string of the contract's form only — ISO 8601 with time and zone (x-template.dateTimePattern). format: 'date', 'dateTime', 'time' and 'daysUntil' show any other string as it is; a date without a zone used to be read as local time.
  • Clarifications of 9.4.0 (six policy rows): a template value the renderer cannot read is an empty place without braces; the plural word agrees with a numeric fallback text when the fallback is shown; number and currency do not apply beyond ±(2⁵³ − 1), and no plural word agrees with such a number; a missing plural form is other, never a neighbouring form; with no number data for the language, numbers are shown as they are and "days left" in plain digits.
  • A string of a kind this renderer does not know is an empty text (unknownStringKind: 'render-empty') — what the renderer already did, now a named rule. Documents of 9.4.0 read by 0.3.0 show such places empty and numbers without separators.
  • kSupportedSchemaVersion is 9.4.0.

0.3.0 #

Reads document contract 9.3.0 (theme contract unchanged, 4.0.1). Icon set 1.0.0.

  • The icon library (schema 9.3.0). Icon.name and the icon slots of ListTile take 332 names instead of ten, and the drawings are data of the contract (contract/icons.json): the renderer draws from a generated copy built into the package, checked against the artifact by a test. Every reader of the contract now draws the same picture the manager saw in the builder.
  • Changed look: the ten original icons (star, trophy, gift, target, clock, check, chevron, user, info, lock) are no longer Material Icons. They are the drawings of the builder's canvas: a filled star, cup, lock and silhouette, a bolder check and chevron. The chevron of a list row, the chevron of an Accordion (it now points to the end of the line and turns down when open, instead of a "down" glyph that flipped up) and the back arrow of AppBar are the contract's chevron too. Screens you already published look different in this version; that is the purpose of the change.
  • Icons are drawn as vector paths by the package's own reader of the icon format (seven SVG elements, every path command), not by flutter_svg and not from a font. The icon is on screen in the first frame, takes the foreground colour of its node (style.color, own or inherited) for both stroke and fill, and a drawing is parsed on first use and kept. The stroke grows with the icon. The box of an icon and the size of its glyph did not change.
  • Right-to-left: the 29 icons of x-mirroredIcons (arrows, chevrons, log-in, undo, send, list, …) are mirrored, once, by the renderer. Everything else is not (play, refresh-cw, list-ordered, truck).
  • Breaking (behaviour): a name the renderer does not have no longer becomes star. It leaves an empty box of the same size (unknownIcon: 'empty-box-same-size'), in Icon and in both slots of ListTile. The same for a value that is not a string — an object, a number, null (unreadableIconValue: 'as-unknown-icon'); the document is still read whole. A missing Icon.props.name is still the default of the field, and a missing trailing.icon is still "no icon, no box".
  • New: kIconSetVersion (1.0.0) — the version of the drawings built into the package. It is independent of the document contract version, and a document does not carry it.
  • New file in the package: THIRD-PARTY-NOTICES.md. The drawings, except the ten original ones, are from Lucide (ISC License; some derive from Feather, MIT License). Lucide is not a dependency of the package.
  • kSupportedSchemaVersion is 9.3.0.

0.2.0 #

Reads document contract 9.2.0 (theme contract unchanged, 4.0.1) and data dictionary 1.1.2.

  • A list row now carries its record (schema 9.2.0, "which record does the screen opened from a row show"). An action carries the record of its node; navigate from a list row opens the screen with that row's record; a page opened by a button outside a list opens without one, a modal sheet keeps the record of the screen beneath; going back drops the record with the screen. Data layers of a screen, strongest first: record → the user's choice in a bound input → scopeFor → data; null and a missing key mean "none" and the next layer answers.
  • New: renderDocument(onRecordAction: …), ScreenFlow(onRecordAction: …), renderScreen(onRecordAction: …, record: …) and the typedef SduiRecordActionCallback = void Function(ButtonAction action, String nodeId, Map<String, Object?>? record). The record is a flat, unmodifiable map of data paths ({'item.id': 'bottle', 'item.name': 'Bottle'}), the same shape as source.record in @hotlayout/react; only keys that have the form of a data path are kept. When both onAction and onRecordAction are given, only onRecordAction is called. onAction keeps its two-argument signature, so existing hosts compile and behave as before (a new optional input instead of a changed typedef, because a two-parameter function is not assignable to a type with a third parameter).
  • Breaking (rare): ScreenScopeRequest gained a field, record — the record the screen was opened with (null when it was opened without one). Hosts that only read request.params / request.read are not affected; hosts that build or destructure the record by pattern must add the field.
  • Breaking: the path params.* is gone. It was a private extension of this renderer that the contract never had (and that the builder does not let a document use). navigate.params are a free map of strings for the host (ScreenScopeRequest.params, ButtonAction.params); they are not data of the screen. A row of a list now tells the host which record it is through record, not through parameters, which are the same in every instance of a row template.
  • Breaking: the record of a list row is stronger than what the user chose in a bound input (it used to be weaker, so item.selectedId was read from the session for every row). A ChoiceGroup bound to a path the record also carries still shows the user's pick for as long as the value under its binding is unchanged.
  • Language choice, refinements of contract 9.1.0: the locale for dates is the longest well-formed prefix of the requested tag (ja_JP_JP → ja-JP); LocaleResolution.directionLocale is a canonical tag (ks_IN → ks-IN); a tag longer than 100 characters is not a tag; a language listed in localization.locales without a single translated string is not a language of the document (DocumentLocalization.availableLocales).
  • Data dictionary 1.1.2: the sample images of item.image are data:image/webp.
  • kSupportedSchemaVersion is 9.2.0.

0.1.0 #

First version prepared for integrators. Reads document contract 9.1.0 and theme contract 4.0.1.

  • One public library, package:hotlayout_flutter/hotlayout_flutter.dart. Added to it: DocumentLocalization, LocalizedStrings, LocaleResolution, directionOfLocale, isRtlLocale, VersionVerdict, kSupportedSchemaVersion, kSupportedThemeContractVersion, kDocumentFetchTimeout, kThemeFetchTimeout, ScreenPresentationToken, UiNode.
  • RendererLabels: the renderer's own words (back arrow, failed image, version notices) are English by default and can be replaced through the new labels parameter of renderDocument and renderScreen.
  • DocumentLoad.detail, ThemeLoad.detail, the default title of PublishedDocumentSource and debug logs are English; logs are prefixed with [hotlayout].
  • CollectionCatalog and CollectionSource are no longer exported: they are host code of the demo app, not part of the renderer.
  • example/: the quickstart as a complete app.
  • Device locale matching of contract 9.1.0: ar-EG finds a document's ar; never the other way round.
  • The package declares no assets. The demo app moved out of the package root into its own app (demo/ in the repository, not published), so contract fixtures no longer travel into the bundle of every host app.
  • Breaking: ThemeLoad.fromBundle requires assetPath — the default pointed at an asset of the demo app and always gave assetMissing in a host. ThemeLoad.fromNetwork takes an optional assetPath; without it the fallback is the built-in theme.
  • Breaking: BundledDocumentSource.fixture is removed (it named assets of the demo app).
  • The system back button is connected to the document's screen stack. ScreenFlow (and renderDocument) handle it through PopScope: one screen back inside the document while there is one, your route on the first screen; predictive back on Android 14+ gets the right animation. New: ScreenFlowController (canGoBack, currentScreenId, goBack(), a Listenable), ScreenFlowBackCallback and the parameters controller, onSystemBack, handleSystemBack of renderDocument and ScreenFlow.
  • Breaking: VersionVerdict.majorMismatch is split into newerMajor and olderMajor; isMajorMismatch answers the old question. The version notice now has two texts: RendererLabels.updateRequired ("Update the app to see this screen.") only for a document of a newer major, and the new RendererLabels.screenUnavailable ("This screen is not available right now.") for a document of an older major or with an unreadable version — updating the app does not help there. RendererLabels.unreadableVersion is removed (renamed to screenUnavailable). The reason and both versions go to the debug log and to DocumentLoad.detail.
0
likes
0
points
66
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter renderer for HotLayout server-driven UI. Draws screens published by the HotLayout builder from a JSON document, a theme and your data.

Homepage
Repository (GitHub)
View/report issues

Topics

#server-driven-ui #sdui #ui #widget

License

unknown (license)

Dependencies

flutter, flutter_svg, http, intl, vector_graphics_compiler

More

Packages that depend on hotlayout_flutter