Skip to main content

hotlayout_flutter 0.31.0 copy "hotlayout_flutter: ^0.31.0" to clipboard
hotlayout_flutter: ^0.31.0 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.31.0 #

The contract version is unchanged (9.28.0). One rendering fix; nothing changes for a document without a shadow under a see-through background.

  • A shadow is painted only outside its box. A node with a shadow and a background that lets light through (a translucent colour, a gradient with a translucent stop, or backdropBlur) used to show the blurred shadow through the box, tinting the card. CSS never paints a box-shadow inside the border box, and the other readers do not; the reader now clips the shadow to the outside of the box (the radius included). An opaque background is drawn as before.

0.30.0 #

The contract version is unchanged (9.28.0). One new optional parameter; nothing else changes for a host that does not pass it.

  • onReport on renderDocument and renderScreen: what the reader ignored, replaced or refused on its own now reaches the host in release builds too, not only the debug log. HotlayoutReport is a sealed class with the kinds the other readers of the contract share: IgnoredInputReport (where, reason: an unknown widget or token, a refused picture address, a write path the document had no right to name, a list cut by the budget, a screen deeper than the parse limit, a node of the wrong shape), IncompatibleDocumentReport, NewerMinorDocumentReport, UnknownEntryReport, InvalidThemeValuesReport (paths), ThemeFallbackReport and UnknownThemeIdReport. Each has kind and message.
  • Once per cause. A report is delivered once for the life of the widget: a rebuild, new data or another locale does not repeat it. The list budget used to print on every build; it is now one report per screen. At most 50 distinct reports are delivered per widget. No report carries a value from your data.
  • Findings made before the widget exists (parsing a document, reading a theme) travel with the parsed object: UiDocument.reports and RenderTheme.reports. renderDocument delivers both; renderScreen delivers the theme's, so a host that draws single screens reads UiDocument.reports itself. ThemeContract.themeById for an unknown id and a ThemeLoad that fell back return themes that carry the matching report.
  • The debug log lines are kept, printed once per distinct message.

0.29.0 #

Reads document contract 9.28.0 (theme 4.0.1, icon set 1.0.0 and data dictionary unchanged). The version adds no field: it states rules of the reader. A document that stays inside the limits is drawn exactly as by 0.28.0.

  • A picture body is read up to 10 485 760 bytes (10 MiB; kDefaultMaxDownloadBytes was 10 000 000). Redirects (at most three, each target judged again by the address policy), the 30 seconds for a picture with its redirects, and the decode of the longer side at no more than 4096 pixels with the proportion kept are as in 0.28.0.
  • The host's decision cannot make a script a picture. An address that resolveImageUrl returns and that a URL parser reads as javascript: or vbscript: (leading controls and spaces stripped, tab and line breaks removed, any case) is refused like any other refused address.
  • A refused picture is reported in debug builds: the address of the document up to 120 characters, for an address from your data only its scheme and host.
  • A list budget. A screen draws at most 2550 instances of list templates, nested lists included, counted in document order, depth first. The count follows the whole tree of the screen for its width class, not what is shown now: a hidden list, a list in an inactive tab or in a folded block counts the same. The instance that would pass the limit is not drawn, nor anything inside it or after it on the screen. A debug line says how many instances were left out. A modal sheet and the page under it have a budget each.
  • Numbers of a theme outside the schema bounds, or not finite, no longer drop the token. The value of the same token and field in the built-in theme of the same brightness is used for spacing, radius, font size, line height, font weight and letter spacing; every other number (a custom token, a field the built-in token lacks, a shadow layer, a gradient stop) takes the nearest bound. RenderTheme.numberReports lists every replacement as <place>: built-in or <place>: bound. A value that is not a number still leaves the token out.
  • A text that paints nothing says nothing. A text whose colour (its own or inherited) is fully transparent, or that is inside a node whose resolved opacity is exactly 0, is out of the semantics tree, so it is not part of the name of a pressable card. A card and a control keep their own names. 0.01 is still heard.
  • A sheet with dismissible: false takes no focus trap and no Esc; the system back stays with the host. Nothing changes for a layer that can be dismissed.

0.28.0 #

Hardening release: the document, the theme and the data arrive from the network, and the package now treats all three as untrusted input. The contract version is unchanged (9.27.0). New parameters are optional; the changed defaults are listed first.

Changed defaults

  • A picture is fetched only from an https address whose host is a name (a host with an alphabetic top-level label, the host form of openUrl). An IP address in any notation (127.0.0.1, 2130706433, 0x7f000001, 169.254.169.254), an IPv6 address, a single-word host such as localhost, an address with user info and every other scheme draw the "image unavailable" placeholder and make no request. This covers pictures from the document and from your data, raster and SVG. data:image/… addresses are not network requests and still draw (see the size limit below). If your pictures live on an internal host, pass resolveImageUrl.
  • A picture body is read up to 10 MB (maxDownloadBytes). A larger Content-Length is refused before the body is read, and a body that does not declare its length is cut at the limit.
  • A picture is decoded at the size of its box, never above 4096 pixels on a side and never enlarged, so a small file that declares a huge size cannot allocate gigabytes of pixels.
  • A picture request gives up after 30 seconds and sends no cookies, no credentials and no headers. Redirects are followed by hand, at most three, and every target goes through the address policy again.
  • A document or theme body is read up to 4 MiB (four times the contract's 1 MiB budget) by HttpDocumentTransport. A larger answer is a malformed load with the bundled copy (DocumentLoadStatus.malformed, ThemeLoadStatus.malformed).
  • Theme numbers are read within the bounds of the theme schema (font size 8…96, line height 0.8…3, letter spacing −2…8, font weight 100…900, spacing 0…128, radius 0…256, shadow offset and spread −64…64, shadow blur 0…128, opacity and gradient position 0…1). A token with a number outside its bounds, or one that is not finite, is left out as if it were malformed, and the built-in value applies.
  • ThemeLoad.fromJson, parse and fromNetwork never throw: a body the reader cannot handle is a malformed load with the built-in theme.
  • Nothing from your data is written to the log any more: the SVG failure line (which used to carry the address, in release builds too) now names only the class of the failure (HTTP 404, timeout, parse error, …) and is printed in debug builds only; the debug notices about an unknown icon name and an unmatched choice no longer print the value.

New parameters of renderDocument, renderScreen and ScreenFlow

  • resolveImageUrl: String? Function(String url): your decision about every picture address, data: addresses included. Return the address to fetch (it may be rewritten) or null to refuse it. A policy that throws refuses. It is called again for every redirect target.
  • maxDownloadBytes: int: the most bytes of one picture body (default 10 000 000).
  • writablePaths: the data paths the document may write, as a bool Function(String path) or an Iterable<String>. A write to any other path (by a field or by setValue / toggleValue) reaches neither the renderer's session nor onWrite. Without it every well-formed path is allowed, as before.

Fixes

  • A number that is infinite (1e400 in JSON) in maxLength, titleMaxLines, subtitleMaxLines, a field radius or the size of a part no longer throws: an integer field takes a number only when it is finite and whole. The same guard covers the other places where a document number becomes an integer (filled steps of a segmented bar, the fill line of a bar with steps, the dashes of a dashed border).

0.27.0 #

Reads document contract 9.27.0 (theme 4.0.1 and icon set 1.0.0 unchanged; data dictionary 1.7.0). Two minors in one release: 9.26.0 (a long press, a grouped list, a sheet that closes by a drag) and 9.27.0 (the record of a page follows the data). A document without the new fields is drawn exactly as by 0.26.1, except that a modal sheet now follows the finger down (see below).

  • A long press (longPress and longPressLabel on a Column, Row, Container, Stack and ListTile). It has the form of action and runs the same way: navigate inside the document, everything else to onAction / onRecordAction with the record of the node, and the haptic request, the analytics event and the action of the long press in that order. It fires 450 ms after the finger went down, while it is still down, and the lifted finger presses nothing. A finger that moves farther than 10 dp first is neither a long press nor a press. A node without a longPress takes a long hold as a press, as before. The innermost control takes the hold as it takes the press, so a button in a card keeps its own press and a row with its own long press inside a card gets it. A card or a row with only a long press is still a control (focused, named, sinks under the finger, its tap does nothing); a write action on a long press is not run; a row that is an option of a choice does not run it. The pressed pose stays while the finger is down. Accessibility: a custom action named by longPressLabel (the platform's own long press action when the name cannot be heard); the context menu key and Shift+F10 on the focused node perform it at once.
  • A grouped list (Repeater.groupBy with a second child as the header). Records that follow one another with the same value under the path form a group, made of the records the list draws (after where and limit); the header is drawn before each group in the scope of the group's first record, and a press inside it carries that record. Absent, null and "" are one value, and 1 and "1" are two. Headers and records stand in one flow with one gap; a header hidden by its condition takes no place; numbered items count records only. A grid, a row, a carousel and a list without a header child are drawn without headers.
  • A sheet that closes by a drag down. A modal sheet from the bottom (not a window in the middle) follows the finger down when its body is scrolled to the top (otherwise the body scrolls first) and never goes above its place. Released at 0.3 of its height, or thrown down at 700 dp per second, it leaves in 200 ms and only then does the stack go one step back (back event); a fling up, or a shorter drag, returns it in 250 ms. The scrim fades with the part of the sheet still seen. The finger that dragged the sheet presses nothing under it. Under MediaQuery.disableAnimations the sheet still follows the finger, and the return and the exit are instant (as with HotlayoutTransitions.none).
  • dismissible: false on the entry of a modal screen turns off the drag, the tap on the scrim and Esc. The system back stays with the host, and a navigate.back inside the layer closes it. ScreenEntry.dismissible is the new field.
  • The record of a page follows the data. The record a frame of the stack holds (opened from a list row, inherited by a sheet over it, carried by carryRecord) is now the chain of the rows it came from. Whenever you give new data, every frame looks for its rows by <item>.id, first in the collection the list read and then in the other collections the document names for the same entity, and takes the first match whole; a row found nowhere keeps the last record it had, and a row with no id stays as it was opened. The stack, onScreenShown and the analytics do not change; the screen keeps its scroll, carousels and typed text; a changed number counts to its new value (animateChanges); the record handed to onRecordAction is the refreshed one.
  • Shared tables run by this release, every row by the rule and, where a screen can show it, through real widgets: long-press-cases.json (gestureCases 11, keyCases 6, nodeCases 9, receiverCases 5, documentCases 7, ruleCases 5), repeater-group-cases.json (sameCases 11, groupCases 7, listCases 6, documentCases 8, ruleCases 3), sheet-dismiss-cases.json (startCases 8, releaseCases 9, entryCases 2, documentCases 3, ruleCases 6) and the groups entitySourceCases (3), liveRecordCases (23) and liveFlowCases (6) of record-scope-cases.json. Fixtures longpress-groups-sheet-screens and live-record-screens are in the fixture catalog.
  • For widget tests: a hold of 450 ms needs pump(Duration) (the timer is a real Timer); a drag inside a sheet loses the first 18 dp to the touch slop, as under any button, because the sheet's own drag stands in the gesture arena next to the body's scroll view. ScreenFlowController.frameRecords (@visibleForTesting) lists the record of every frame of the stack.

0.27.1 #

  • A press is released safely when its node was replaced under a held finger (a step completed in place, a reset): no animation runs on a disposed controller.

0.26.1 #

Fix: a solid style.border on a node with a radius is drawn OVER the content, as in CSS, and the content is clipped by the inner edge of the border (the outer radius minus the border width, never below zero). Before, the border lay under the content and the content was clipped by the outer radius, so a picture or a coloured box filling the card painted over the border on the rounded corners. A node without a border, a dashed border and the shadow and glass of the box are unchanged.

0.26.0 #

Reads document contract 9.25.0 (theme 4.0.1, icon set 1.0.0 and data dictionary 1.5.0 unchanged). Five optional additions; a document without them is drawn exactly as by 0.25.0, except that pressable nodes now sink under the finger (see below).

  • Steps that fill smoothly. While the value of a ProgressBar with segments runs a transition (animateChanges, animateIn) the filled steps are a number with a fraction, U(from) + (U(to) - U(from)) * e, where U is the number of whole steps at rest and e the curve of the transition. The step that is filling is drawn as its track (or currentColor) with the fill over it, cut at its share, with the radius of the step kept; the gradient runs along the drawn fill. At rest the steps are whole, both ends of a transition are pictures at rest, a change inside one step moves nothing, and MediaQuery.disableAnimations gives whole steps at once.
  • The glow of a scale (glow: 'soft' | 'strong' on ProgressBar and CircularProgress): a blurred copy of the paint of the fill (its colour, or its gradient along the fill, with the round ends) lies under the scale. soft is a Gaussian blur of 4 dp at opacity 0.55, strong of 8 dp at 0.9. The track does not glow, nothing filled means no glow, and the glow takes no room and is not clipped by the box of the node. In a bar with steps one step glows: the current one in currentColor when that colour is there, otherwise the last step with any fill. It follows the fill during a transition.
  • A shadow of the colour of the background (style.shadowColor: 'background'): every layer of style.shadow takes the hue of the node's own background and keeps its own opacity. A gradient gives the colour of the stop nearest to the middle (the earlier one on a tie); no background, an image, or a colour the theme does not know leaves the shadow in its own colour. On a Button and a Badge it goes with shadow to the surface of the role.
  • Press feedback. The node that performs the action of a press sinks under the finger: a card, a row and an option to 0.97, a Button to 0.96, and an IconButton, a ghost button and a NavItem fade to 0.7. Down in 80 ms, back in 160 ms with a small overshoot; a press shorter than the way down still reaches the pose. Painting only, no layout change. Only the innermost control answers (a card around a button stays still); no feedback for a disabled button, a "swipe to confirm" button, the keyboard, or a node with pressFeedback: 'none'; MediaQuery.disableAnimations keeps only the fade to 0.7.
  • A haptic request (haptic on an action: light, medium, heavy, selection, success, warning). The new callback onHaptic(HapticWord word, String nodeId, Map<String, Object?>? record) of renderDocument, renderScreen and ScreenFlow is called once per press, before the analytics event and the action of the same press, also for none and for a navigation that finds no target. Nothing for a disabled control, a held key, a drag that has not completed, an unknown word or no field. The reader never vibrates by itself, and the action handed to onAction does not carry the field. HapticWord is exported.
  • Shared tables run by this release, every row by the rule and, where a screen can show it, through real widgets: segmentFillCases (12), segmentFillInlineCases (4), transitionShareCases (5), glowStepCases (7), glowRuleCases (6), middleStopCases (5), tintCases (7), shadow-colour ruleCases (2), press-feedback kindCases (10), poseCases (3), ruleCases (5), hapticCases (7) and hapticSchemaCases (4). Fixture glow-press-screens is in the fixture catalog.
  • For widget tests: a tapped card or button is scaled until a moment after the finger is lifted, so advance time with pump(Duration) or pumpAndSettle before measuring it. The press feedback is made of a Transform inside a FadeTransition; a node's own style.opacity is still the only Opacity.

0.25.0 #

Reads document contract 9.24.0 (theme 4.0.1, icon set 1.0.0 and data dictionary 1.5.0 unchanged). Four optional additions; a document without them is drawn exactly as by 0.24.0.

  • A value that grows when shown (animateIn: true on Text, ProgressBar, CircularProgress). Each time the node is drawn on a shown screen (the first screen, a page pushed or shown again by "back", a section switch, a node that appears by a condition or as a new record) its number grows from zero over the curve, duration and steps of animateChanges, together with the slide of the page. A screen under a modal layer is not shown again when the layer closes, and a change of data is not a show. A scale grows its value (a literal one too) and its max stands at once. With animateChanges a change of data during the appearance leads from the shown value; without it the new value stands at once. MediaQuery.disableAnimations, a value that is not a number and a value of zero give the value at once. The screen reader hears the final value.
  • "Swipe to confirm" stays busy after the action. The action runs at once as before; the thumb glides to the end (200 ms) and stays there with a turning ring of three quarters (800 ms a turn), and a press, a gesture and a key do nothing. The callback you pass as onAction / onRecordAction may return a Future (HostActions.handle returns the Future of its handler): the thumb returns over 250 ms when it completes or fails. Without a Future it returns after eight seconds, unless the screen changed first. none and an invalid standard action return the thumb at once. A disabled button is as before.
  • The motion of the dot on a navigation item. When the condition of badgeWhen changes its answer while the screen is shown the dot appears with a flash (0 to 1.25 to 1 over 320 ms) or fades (200 ms, in place until the end). No motion on the first show or when the screen is shown again, and none with MediaQuery.disableAnimations.
  • The top zone (pin: 'top'). The run of children of the screen root right after the app bar (or first) that carry the word does not scroll; the body starts at the bottom edge of the zone and is clipped there. Counted by the document, not by visibility; not read after a child without the word, on an app bar or a navigation bar, or with bodyTop: 'screenTop'. Without an app bar the safe inset and the root's top padding stand above the first node; the screen gap lies between nodes, less the pull of a later node, and the pull of the first is not read.
  • Shared tables run by this release: appearStepCases (9), appearDecisionCases (8), appearTargetCases (4), appearFlowCases (9), the busy… groups of swipe-confirm-cases.json (4 + 7 + 10), navBadgeMotionCases (6), navBadgeAppearScaleCases (6), navBadgeDisappearOpacityCases (4) and top-zone-cases.json (all five groups, 34 rows), each by the rule and, where a screen can show it, through real widgets. Fixture appear-swipe-dot-screens is in the fixture catalog.
  • For widget tests: a number with animateIn is zero on the first frame, so advance time with pump(Duration); a busy swipe button keeps a ticker for as long as it is busy, so end it (complete the Future, or pump(Duration(seconds: 9))) before the test ends, and do not pumpAndSettle while it turns.

0.24.0 #

Reads the same contracts as 0.23.0 (document 9.23.0, theme 4.0.1, icon set 1.0.0, data dictionary 1.5.0). Screens now move into place the way the platform moves them; the contract is silent about motion, so this is the renderer's own, and a document is drawn exactly as before once the motion is over.

  • Transitions between screens, by the cause of the show. A page opened forward slides in from the end of the line (iOS and macOS: the native navigation push, the previous page drifting a third of the way back; elsewhere: Material "fade forwards"), and "back" plays it in reverse, over 300 ms. A modal sheet rises from the bottom while the scrim and the backdrop blur thicken with it, and now also sinks back when closed (it used to vanish); a centred window grows from 96 % while it fades in, and shrinks away when closed. A section switch of the navigation bar is a 220 ms cross-fade of the new screen over the old one, which stays whole beneath it, so parts that are the same on both screens do not move. The first screen, a new entry and a new document appear without motion. Right-to-left writing mirrors the direction.
  • Tabs: the indicator slides from the old title to the new one while the panels cross-fade.
  • The edge swipe back on iOS and macOS. On a deeper page a swipe from the leading edge reveals the page below under the finger and goes back on release past half the width or with a fling, through the same step as every other "back" (onSystemBack is asked first). A swipe given up changes nothing and reports nothing. Not on the first screen, a modal layer, with handleSystemBack: false or without motion.
  • transitions on renderDocument and ScreenFlow: HotlayoutTransitions.platform (default) or HotlayoutTransitions.none for no motion at all. MediaQuery.disableAnimations always makes every change instant.
  • Motion never changes the flow: onScreenShown is called at the step, exactly once, as before; the stack, the record and the session are untouched; the leaving screen takes no touches and is not announced; a step taken during a transition is taken at once (a "back" while a page is still coming in plays the same motion back). A page revealed by "back" is built anew, as before.
  • For widget tests: after a page step both screens are in the tree for 300 ms, and a closed modal layer for 220 ms. Advance time with pump(Duration) or pumpAndSettle, or pass transitions: HotlayoutTransitions.none.

0.23.0 #

Reads document contract 9.23.0 (theme 4.0.1, icon set 1.0.0 and data dictionary 1.5.0 unchanged). Two minors in one release (9.22.0 and 9.23.0), every field optional; a document without them is drawn exactly as by 0.22.0.

  • A floating navigation bar (NavBar.float: true, only the literal, read where the bar stands at the bottom). The bottom zone lies over the body: the body area reaches the bottom of the screen, the content scrolls under the bar and ends with the height of the whole zone (block, margin box of the bar, safe inset), so the scroll limit stays what it was. The bottom safe inset lies under the box of the bar, outside it, as transparent room. The look of the pill is the node's style (margin, radius, backdropBlur, shadow); nothing is drawn of its own. A touch beside the pill reaches the body.
  • A dot on a navigation item (NavItem.badgeWhen): a circle 8 dp across in accent, centred on the top corner of the icon on the side of the end of the line, in the bar at the bottom and at the top, on the active item as well; it does not resize or get clipped by the item and is not announced. A condition that cannot be read gives no dot.
  • Swipe to confirm (Button.confirm: 'swipe'): a track with a round thumb; the action and its track event run once when the thumb is released at 90 % of the travel or more, and never on a tap. Enter and Space (not their repeat) and the screen reader's "activate" act at once; a disabled button does not move. Right-to-left writing drags toward the left.
  • A dashed border (style.border.style: 'dashed'): dashes of 3 and gaps of 2 border widths along the centre line, following the corner arcs, closing without a seam; on nodes, states, width layers and the surfaces of buttons and badges.
  • A filtered list (Repeater.where): the records that do not pass do not exist for the layout (a grid has no holes, a carousel counts its stops by the drawn cards, numbered rows count the drawn ones, limit counts what passed). A condition may compare with the value of another path (valuePath instead of value, six operators), and isEmpty / isNotEmpty with item and where judge the list after the filter. No operand: the record passes in where, the condition does not hold elsewhere.
  • A gradient on the glyphs of a text (style.textGradient): one transition over the content box of the whole text, not inherited, read by Text only. A part with a colour of its own keeps it; a line and a list marker keep the colour of the text.
  • A compact number (notation: 'compact' beside format: 'number'): 48.2K, 48,2 тис., by the contract's arithmetic and suffix table, not by NumberFormat.compact.
  • Shared tables run by this release, every row by the rule and, where a screen can show it, by a real widget: floatZoneCases (9), floatPlaceCases (7), navBadgeCases (9), navBadgeBoxCases (4), nav922RuleCases (12), swipe-confirm-cases.json (all nine groups, 49 rows), border-dash-cases.json (20), repeater-filter-cases.json (all seven groups, 57 rows), valuePathCases (22), compactCases (41), textGradientCases (9), textGradientPartCases (3), textGradientDocumentCases (8). Fixtures float-nav-swipe-screens and filter-gradient-compact-screens are in the fixture catalog.
  • Not exact: the drag of the thumb is captured by the standard horizontal drag recognizer (the first axis past the touch slop), so a diagonal drag that the table calls "not captured yet" is decided by the arena, not by the comparison of the two distances; the rule itself (swipeCapturedBy) is checked against its table.

0.22.0 #

Reads document contract 9.21.0 (theme 4.0.1, icon set 1.0.0 and data dictionary 1.5.0 unchanged). Four optional additions; a document without them is drawn exactly as by 0.21.0.

  • Numbers that lead to their new values (animateChanges: true on Text, ProgressBar, CircularProgress). When data change under a node that is already shown, the number counts and the scale grows, over 700 ms by the contract's curve 1 - (1 - p)³ (not Curves.easeOut). A number in a text moves in whole steps when both ends are whole, otherwise by the digits of the currency or three, cut towards the old value; it is written by the same format, template and plural word as the final one. A scale fills without a step, the label valueOfMax writes steps and percent the shown ratio. The first show, a node that appears, a new record of a list, a new language, MediaQuery.disableAnimations, a value that is not a number on either side and an unchanged value give the new value at once; a change during a transition starts from the value shown at that moment. The screen reader hears the final value only. Works with live data: a new data map under an open screen leads.
  • countdownPart, a seventh binding.format: one part (unit: days, hours 0–23, minutes 0–59, seconds 0–59) of the time left, rounded down, never below zero, written like number; pad: 2 writes at least two integer digits; a plural word agrees with the part. Without a readable unit the value is shown as it is. A screen that shows a countdown redraws itself every second (a minute when no part is seconds) while it is shown and not pinned by now; a screen under a disabled TickerMode stops and reads the clock afresh when shown again.
  • cap: 'round' on ProgressBar and CircularProgress. The ring's arc ends are half circles of the thickness kept inside the filled part (a dot for a tiny share, the whole ring with no seam when full); a bar's track, fill and every step have a radius of half the thickness, whatever the theme's pill, and a non-empty fill is at least as long as the thickness. A gradient end takes the colour of the gradient at its own centre.
  • An app bar that changes on scroll: styleScrolled (background, color, shadow, backdropBlur) laid last over the bar's style once the body of its own screen is scrolled past max(0, barBottom - bodyTop) (strictly), and AppBar.titleShown: 'scrolled', which keeps the place of the title and its meaning for the screen reader at rest. Only the bar of the screen's zone is read.
  • Shared tables run by this release, every row twice (by the rule and by a real widget): value-transition-cases.json (curve 7, steps 16, digits 7, decisions 12, targets 7), countdownCases of template-cases.json (28), capCases of progress-cases.json (20), the scrolled… groups of media-chrome-cases.json (9 + 4 + 7). Fixture value-countdown-screens is in the fixture catalog (the demo opens it with --dart-define=fixture=value-countdown-screens).
  • A transition runs a ticker for 700 ms and a countdown runs a timer, so in a widget test advance time with pump(Duration); pumpAndSettle settles after a transition.

0.21.0 #

For a live app: data that changes under an open screen, a new publication picked up while the user is in the flow, and the app's own actions routed by type. Reads the same contracts as 0.20.0 (document 9.20.0, theme 4.0.1, icon set 1.0.0).

  • What the user wrote yields to new data at the same path. A value written into a bound input (and by setValue / toggleValue) used to stay above data for the life of the flow, so a live app whose database answered with another value kept showing the old one. Now each entry of the session remembers what data said at its path when it was written and is dropped for good as soon as data says something else there. A host that echoes the written value back changes nothing on screen. The same rule as @hotlayout/react.
  • A new version of the same document keeps what the user wrote, also when it restarts the flow from its entry because a screen of the stack is gone. Another documentId or another entry still starts over. Before, any restart forgot the session.
  • HostActions: a ready onRecordAction. One handler per action type (HostActionCall with params, record, nodeId and valueOf, which reads the parameter and then the record); the standard actions openUrl, deeplink and close through runStandardAction and never through a handler; onMiss with the reason (noHandler, noExecutor, invalid, busy); an asynchronous handler blocks presses of the same type until it completes (exclusive, on by default); errors go to onError or FlutterError.reportError.
  • Tests of what a running app does: new data, a re-parsed copy, a new version with and without the screens of the stack, another documentId, a new theme — the stack, the session, the fields and the show events (live_update_test.dart).
  • README: Live data and new versions (a stream of data, refreshing the publication on a timer and on return to the foreground, ETag), and HostActions under Actions and navigation. The example gained lib/live.dart (drawLive, appActions, EtagTransport, LivePublication, LiveScreen) and uses HostActions; a refresh that falls back no longer replaces the published document in the quickstart.
  • Corrected: the documentation of DocumentLoad.reusing said a new document object resets the screen stack; for the same documentId it does not.

0.20.0 #

Reads document contract 9.20.0 (theme 4.0.1, icon set 1.0.0 and data dictionary 1.5.0 unchanged). Three optional fields, none of which changes the layout; a document without them is drawn exactly as by 0.19.1.

  • Motion of a node (motion, any node but the root screen): float drifts a node along an axis, pulse breathes in scale and opacity. The position is the contract's one formula (a sine of the phase 2π · ((t − delay) mod duration) / duration), driven by a ticker that starts when the node is mounted, so a node that appears again counts from zero. The shift goes toward the end of the line (mirrored right to left) and along the axes of the screen even inside a rotated stack layer; the scale is about the centre of the box; the opacity multiplies style.opacity and the node stays in the semantics tree at zero. Nothing is clipped, nothing is laid out again. MediaQuery.disableAnimations leaves the node at rest and runs no ticker. A form the reader cannot draw (an unknown kind, a missing or extra field, a number out of bounds) is no motion.
  • Blur under a modal layer (ScreenEntry.backdropBlur, 1–40 dp): a BackdropFilter over exactly the area of the scrim, under the scrim, so the screens below with their app bar and navigation bar are blurred and the layer is not. It grows with the layer's appearance and ends at the named radius. A page has no layer, and the field is ignored on it.
  • Glass (style.backdropBlur, 1–40 dp): the blur of what is painted under the box of a node, clipped by its radius, with the node's own background over it and the shadow outside the clip. On Button, IconButton and Badge it lies on the surface of the role, once. A state and a width layer can name it or unset it. Not inherited.
  • The mirrors of x-motion and x-backdropBlur and the two capabilities nodeMotion and backdropBlur are checked against the artifact.
  • Shared tables run by this release: motion-cases.json, all four groups: poseCases (29, by the formula and by a real document whose clock is advanced to the moment), documentCases (28), blurCases (11, by a real flow that opens the screen as a layer), glassCases (11).
  • Fixture motion-blur-screens is in the catalog and the demo (--dart-define=fixture=motion-blur-screens).
  • A moving node keeps a ticker alive: in a widget test advance time with pump(Duration); pumpAndSettle does not settle on such a screen.

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
160
points
66
downloads

Documentation

API reference

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
Contributing

Topics

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

License

Apache-2.0 (license)

Dependencies

flutter, flutter_svg, http, intl, vector_graphics_compiler

More

Packages that depend on hotlayout_flutter