Skip to content
Flow-UILive

Resilience

One bad widget never blanks a screen. How Flow-UI decodes defensively and reports precisely.

In a server driven app the payload is a moving target: backends ship new widget types, fields evolve, and sometimes data is just wrong. Flow-UI's decoding is built around one promise: the page always renders.

What happens to problem widgets#

ProblemBehavior
Unknown typeThe widget becomes a placeholder. Debug builds show a labelled card; release builds skip it silently.
Payload fails to decodeSame containment, and the error records the exact missing key and path.
Array element is garbageThe element is dropped; the rest of the array survives. This holds for widget payload arrays too, so one malformed button does not empty a button_row.
Unknown arrangementThe section falls back to vertical.
Two widgets claim the same idThe later one is renamed (cart_row then cart_row#2) and reported, because duplicate ids break list rendering and make mutations ambiguous.

Every one of these is recorded in DecodeDiagnostics with the widget type, the coding path and a readable message like Missing key 'title' at page.sections.Index 0.widgets.Index 3.data.

Identity is part of resilience#

A widget with no id is given one derived from its position in the response, not a random one. That matters more than it sounds: AnyWidget is Identifiable and drives ForEach, so an id that changed between two decodes of the same payload would make SwiftUI rebuild the whole page on every refresh instead of diffing it, taking scroll position and in flight image loads with it.

Send a real id anyway for anything interactive, stateful or replaceable. The positional fallback is the floor, not a substitute.

Why this is a feature, not a fallback#

Forward compatibility becomes free. A backend can ship hologram_projector widgets to every client today; old app versions skip them cleanly, new versions render them. No version gating, no crash reports, no coordination meetings.

Three of these five widgets are brokenjson
{
  "widgets": [
    { "type": "image_text_card", "data": { "title": "I decode fine" } },
    { "type": "hologram_projector", "data": { "beam_strength": 11 } },
    { "type": "image_text_card", "data": { "not_title": "missing required key" } },
    { "id": "typeless", "data": { "title": "no type at all" } },
    { "type": "image_text_card", "data": { "title": "I also decode fine" } }
  ]
}

Rendered result: two healthy cards, one orange unknown placeholder, one red malformed card naming the missing key, one silent drop. The page lives.

Compatibility rules for backends#

  1. New widget types can ship any time; old clients skip them cleanly.
  2. New optional fields on existing payloads are always safe.
  3. Never repurpose an existing key's meaning; add a new key instead.
  4. Keep id stable across responses for widgets that mutate or hold state.