Skip to content
Flow-UILive
On this page

The Flow-UI JSON envelope

The Flow-UI JSON envelope is a page with optional header and footer, sections, and widgets. One document describes the whole native screen.

Ayush MishraPublished 6 min read

The Flow-UI JSON envelope is a page with optional header and footer, sections, and widgets. One document describes the whole native page (searchers say screen; the type is page). The envelope API is the field list. This article is why those fields sit together in one document instead of five endpoints.

The Flow-UI mark: two nodes joined through a filled centre

One document, one fetch#

PageLoader returns one Data blob per PageRequest. The decoder expects a page object (often wrapped as { "page": { ... } }). Splitting nav, header, and sections into three round trips means three failure modes and no atomic paint. If you must stitch, stitch on the server.

envelope.jsonjson
{
  "page": {
    "id": "home",
    "nav": { "title": { "text": "Home" } },
    "header": { "sticky": true, "widgets": [] },
    "sections": [],
    "footer": { "sticky": true, "widgets": [] },
    "pagination": { "has_more": true, "postback": { "cursor": "abc" } },
    "refresh": { "pull_to_refresh": true }
  }
}

id is required in spirit even when decode can synthesise one. Send it. Everything else can be absent and decode with defaults.

DecodeEnvelope.swiftswift
let decoder = FlowDecoder.make(widgetDecoding: registry, diagnostics: diagnostics)
let page = try decoder.decode(PageResponse.self, from: data).page

PageStore is the usual caller. You rarely decode in a view.

swift
let store = PageStore(pageID: "home", loader: loader, registry: registry)

NavModel has title, subtitle, background color, left button, right buttons. Left button defaults its action toward dismiss in the contract. This is a bar, not an arbitrary section. If you need a full widget in the top chrome, that is the page header strip, not nav.

Bars are widget strips#

PageBar is widgets plus sticky. Sticky true: pin outside scroll (filters, cart). Sticky false: scroll with content. Same widget types as sections. Same registry. Same lossy array.

Sections are the body#

Decoded lossily. Arrangement lives on section layout. Optional section header widget can pin. This is the bulk of the envelope and the bulk of decode diagnostics.

Pagination and refresh belong on the page#

has_more plus opaque postback means the client does not parse your cursor. pull_to_refresh is a boolean the store honours. Putting pagination only on a child widget would force every list widget to become a page. Keep it on the envelope.

What the envelope is not#

It is not your order history resource. Domain JSON can be referenced from widget data (order_id) without copying the whole domain model into the page.

It is not a style sheet. Layout sits on widgets and sections. Global theme is ThemeProvider plus color tokens, not a second document you fetch first (unless your host loader concatenates, which you should not need).

It is not executable. See the 2.5.2 article. Keep it declarative.

Versioning the envelope#

Additive keys are safe. New widget types are safe for old clients (skip). Renaming sections to rows is a breakage. There is no required version header in Flow-UI. A host may send capabilities. That is your convention, not a package feature. Do not invent a schema_version field in this article as if the decoder required it.

Sheets are a sibling document#

Sheets have their own model (detents, header, sections, footer) under sheets. Same nouns, different presentation. open_bottom_sheet carries that fragment. It is not a second page fetch unless your host handler chooses to fetch first and redispatch.

Nav is optional. Omit it and FlowPageView skips the bar. Header and footer are optional. Pagination is optional. Refresh is optional. The only thing you must have to paint something useful is at least one widget somewhere in allWidgets. An empty widget list becomes PageStore.State.empty.

How decode fills gaps#

Missing page id becomes page@ plus the coding path via FlowIdentity.positional. Missing section ids follow the same idea with prefix section. Missing widget ids use the widget type as the prefix. Duplicate widget ids on the page are renamed id#2. Nested accordion ids are not part of that pass.

LossyArray wraps sections, bar widgets, and section widgets. A dropped element is diagnostics, not a failed envelope. That is why one bad promotional row does not take down home.

What belongs in widget data versus the envelope#

Envelope: structure, chrome on the widget wrapper, actions, tracking, pagination, refresh. data: fields only that type understands. Do not put has_more on a list widget hoping the store will see it. The store reads page.pagination.

Domain records (an order, a user) can be referenced by id inside data. Copying the whole domain graph into every page makes cache invalidation miserable. The envelope is UI. Your resource APIs can stay beside it.

Read the field list in the page envelope and the narrative in pages, sections and widgets. The mental model is why the nouns stay at three.

Envelope fields you will actually send#

nav.title and nav.subtitle are TextData. nav.bg_color is ColorData. left_button and right_buttons are ButtonData. Header and footer widgets arrays are lossy. sticky defaults to true if omitted. Pagination postback is JSONValue: a string cursor, an object, whatever the backend already uses. Echo it on PageRequest.Kind.nextPage. Do not parse it in the client unless you are debugging.

refresh.pull_to_refresh defaults to true in RefreshModel's memberwise init, and the JSON key is pull_to_refresh. If the backend omits refresh, the page view does not assume a gesture. Check the decoded model, not your memory of the default initializer.

Atomic paint versus five endpoints#

Teams sometimes fetch nav from a chrome service, widgets from a feed service, and footer from a cart service. That is three failure modes and a loading state that cannot show a coherent page. The envelope exists so PageLoader.loadPage returns one Data and PageStore publishes one state. If you must stitch, stitch on the server. The client architecture is one document.

Actions are not a top-level envelope script. They hang on widgets and on nav buttons. ActionData.type is a free string. Built-ins: toast, dismiss, refresh_page, open_bottom_sheet, api. The package does not HTTP. The package does not route.

The Flow-UI JSON envelope is one page document. Fetch it once. Decode it lossily. Render it natively.

PageResponse is { "page": { ... } }. If your backend returns the page object at the root, either wrap it on the server or decode a different root in a host adapter. The store expects PageResponse.page. Do not fight that in every widget. Fight it once in the loader if you must, then stop.

A home document that stays one fetch#

Home usually needs a nav title, a sticky filter strip, a carousel of promotions, a vertical list of stories, and a footer bar for cart. That is one page with nav, sticky header, two sections (carousel then vertical), and sticky footer. It is not five view controllers. It is not a screen type. Pagination belongs here if the vertical list continues. postback can be { "cursor": "abc" } or a string. The client echoes it.

If promotions fail to decode, LossyArray drops them. Stories still render. If a new type appears in the carousel, old binaries skip it. The envelope did its job: one document, partial success, native paint.

Do not put onLoad scripts on the page. If you need data after paint, refresh, an api action, or a host call you already own. Keep the envelope declarative.

More on the blog.

Read the source

Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.

GitHub

Get started with Flow-UI

Install the Swift package, register a widget, and render a page from JSON.

Get started