Dynamic UI Architecture — Big Picture

Updated: 2026-07-15 23:22 ET · Audited: 2026-06-26

TL;DR: ALPA Mobile has a server-driven UI layer (dynamic feed) where the backend composes pages and the client renders them using a fixed library of Razor components. This doc covers the overall architecture, the current REST contract, and evaluated future paths (real-time push, OTA updates). For field-level contract details see the Dynamic Feed API contract. New to the system? Start at the Dynamic UI Hub — a first-stop guide that walks the whole pipeline through one live screen.

1. What This Is

Certain screens in ALPA Mobile are server-driven: instead of the app hard-coding what appears on screen, the backend delivers a page definition (which containers, which items, in what order) and the client materialises it. Think of it as a lightweight CMS — a Blazor-based admin surface lets content editors compose pages, and the device renders exactly what the editor built, because both sides share the same Razor component library.

Not all screens are eligible. Pages with fixed regulatory requirements, complex domain interactions, or platform constraints remain statically defined. Only screens that are explicitly opted in receive a server-driven layout.

1.1 Big Picture — Hybrid UI

Admin / Editor surface (future — Blazor Server; RemoteHost prototype) Content editors compose pages containers · items · order · MEC branding Live preview renders with the same Razor component library ALPA Gateway (server) Dynamic Feed API PageDto → ContainerDto[] → ItemDto[] · AB#2133, not built Theme endpoint GET /api/theme/{mecId} · 45-token MEC map MobileContent API (live) MenuItem (navigation) · DocumentItem (content) Pilot device — ALPAMobile (MAUI head) BlazorWebView Razor component library ALPAMobile.Presentation RCL (Components/Library/ — since 2026-07-15) (WI-2254/D61 — planning codename “HybridUi”) CardHero · CardSmall · Carousel · Button … factories · ComponentView dispatcher server-driven screens (Home, MEC pages) render exactly what the editor built Native XAML screens (static areas) Jumpseat · KCM · FTDT · Notifications … hand-rolled by design — not server-driven compose / store JSON One shared Razor component library — the admin preview and the device render are the same components, so “what the editor sees is what the pilot gets”

2. Three-Level Hierarchy

PageDto
 └── ContainerDto[]          ← layout: Carousel, Grid, ButtonGroup, Stack
      └── ItemDto[]          ← leaf items: HeroCard, CardSmall, ButtonCard, … (renamed from WidgetDto — D39)

The container layer owns all layout authority — background theming, column count, section headers, "View all" links. Individual items never carry layout properties. Standalone items (e.g. a single EmergencyButton) are wrapped in a bare "Stack" container with one item; the wire format is uniform.

3. Rendering Pipeline

Backend API
  → PageDto (JSON)
  → PageFactory → PageViewModel
  → Home.razor iterates PageViewModel.Components
  → ComponentView.razor dispatches container type
  → Carousel.razor / GridContainer.razor / … (Level 1)
       → ComponentView.razor dispatches item type
       → CardHero.razor / CardSmall.razor / … (Level 2)

ComponentView.razor is the same single dispatcher at both levels. PageViewModel sits outside the ComponentViewModel hierarchy — it is the page root, consumed directly by Home.razor, never passed into ComponentView. (Decision: D27)

Dynamic Feed API — page payload (AB#2133; mock services stand in today) PageDto → ContainerDto[] → ItemDto[] · camelCase JSON, case-insensitive binding Factories (pure mappers — RawRepresentationFactory rule) PageFactory → PageViewModel · ItemComponentFactory: ItemDto.ItemType → ComponentViewModel canonical §3.3 fields first, legacy fallback (D60) · unknown type ⇒ NotSupportedException Home.razor iterates PageViewModel.Components (page root — outside the ComponentViewModel tree, D27) ComponentView.razor — single type dispatcher same switch at both levels: containers first, then their items Container renderers (L1) Carousel · GridContainer · Stack · ButtonGroup Item renderers (L2) CardHero · CardSmall · CardText · Button … Rendered screen in the BlazorWebView what the editor composed is what the pilot sees Theme endpoint — GET /api/theme/{mecId} fetched at login · 45 resolved tokens · ?since= re-check Per-MEC token cache theme:{mecId} · evict on themeVersion change CSS custom properties on :root "Surface/Brand" → --surface-brand (injected style block) token names in the payload (BackgroundToken: "Surface/Brand") resolve at render time — D26/D37; the page payload never carries raw colors or sizes

3.1 Content → Component Adapters

The dynamic feed is one of three content sources that hydrate the same component library. Every mapping goes through a dedicated pure-mapper factory (never inline field assignment in a page) — see N-TIER-ARCHITECTURE § Content-to-Component Mapping and decisions D58/D60. All three factories are contract-tested (ItemComponentFactoryTests · ContentComponentFactoryTests).

Dynamic feed ItemDto ItemType discriminator · AB#2133 (mocks today) MenuItem MobileContent (live) DocumentItem MobileContent (live) Favorite ItemTypeId + ItemId discriminated reference carrier/* APIs MEC reps · events · committees legacy native path — no adapter resolves via IMenuQueries / IDocumentsQueries ItemComponentFactory every §5 discriminator + legacy aliases (D60) MenuItemCardFactory image check + icon mask DocumentHeroFactory + OpenRoute (P1 policy) native page ViewModels (MyPilotGroupPageViewModel) ComponentViewModel scaffold (domain-free — D6/D11) CardHeroViewModel · CardSmallViewModel · CardTextViewModel · ButtonCardViewModel · CarouselViewModel … ComponentView.razor → CardHero / CardSmall / … renderers one library, whichever source fed it bypasses the component library today — MobileContent migration undecided (see MEC customization mock guide)

4. Theming

BackgroundToken and PaddingToken in the page response are semantic key names only (e.g. "Surface/Brand", "Spacing/Small"). Resolved values come from a separate theme endpoint queried on login and cached client-side. The theme endpoint delivers MEC-specific override values against Figma-sourced base token keys. The page response never carries raw colour or size values.

Client-side cache is keyed on PageDto.PageId for pages and ItemDto.Id for individual items. No HTTP ETag until the admin UI is built.

Token definitions: design-tokens.html

5. Current Design — REST Contract (In Flux)

Status: Working design — endpoint not yet built, contract not finalised. Work item: AB#1821.

The current working model uses a REST endpoint that returns a PageDto JSON payload. The three-level structure (PageDto → ContainerDto[] → ItemDto[]), the container and item discriminator strings, and the factory skip rule for unknown types are captured in the component spec as a baseline for design alignment — the full contract (route, auth, versioning, caching headers) is still being defined with the backend team.

Working design detail: Dynamic Feed API contract

6. Future Path — Component Portability

Because these are standard Razor components, the same library is portable to:

This portability is gated on the Presentation Extraction work (Feature AB#2087) that isolates the UI into a clean layer.

7. Future Path — Real-Time Push via SignalR (Reviewed)

Verdict: viable, but REST cannot be fully eliminated.

Microsoft.AspNetCore.SignalR.Client is supported on net10.0-ios and net10.0-android in Blazor Hybrid apps. The push pattern works:

  1. Client opens a persistent WebSocket connection to a SignalR hub
  2. Server pushes a PageDto-equivalent payload when content changes
  3. Client calls StateHasChanged() — component tree re-renders. No REST poll needed
ConstraintDetail
iOS background killiOS terminates the SignalR connection when the app backgrounds. No message queue — missed pushes are gone. Connection must be torn down in OnSleep, re-established in OnResume.
No queueIf the client misses a push (drop, background), the update is lost.
Reconnect ownershipWithAutomaticReconnect() helps but re-subscription to hub methods after reconnect is manual.

Realistic split: SignalR for live push + a REST seed or hub GetState() call for initial load and iOS reconnect recovery. Not a pure-SignalR replacement.

8. Future Path — OTA / Binary Update Delivery (Not Viable on iOS)

Verdict: not a viable path for ALPA Mobile's iOS target. The current REST contract is already the correct compliant approach.

CandidateOutcome
Remote BlazorWebView content (point at hosted URL)Not supported — BlazorWebView always serves from bundled local assets. GitHub issue #24821 open, unresolved. Loading a remote Blazor Server/WASM app in a plain WebView is possible but Apple Guideline 4.7 (updated Nov 2025) now requires mini-apps served via WebView to go through full App Store review.
Blazor WASM lazy assembly loadingNot applicable in MAUI — LazyLoadAssemblyLoader relies on the browser JIT. Blazor Hybrid runs on the native .NET runtime; no equivalent API exists.
MAUI OTA equivalent (CodePush / Shorebird)None exist for .NET MAUI. App Center CodePush was React Native only and is retired.
Assembly.LoadFrom() at runtimeBlocked on iOS (AOT-only, no JIT). Technically possible on Android but Play Store policies restrict downloading new executable logic for core functionality.

Why the current approach is already correct: the REST-based server-driven feed pushes data and layout decisions (PageDto → ContainerDto → ItemDto), not code. The full component library ships with the app binary. This is the only reliably App Store-compliant path for delivering dynamic UI on iOS — Apple Guideline 3.3.2 permits OTA updates for interpreted content (JSON data, layout) but blocks new compiled/native code outright.

9. Cross-References

DocumentLocation
API contract + field-level specdynamic-feed-api-contract.html
Naming decisions D27–D30naming-decisions-record.html
Design tokensdesign-tokens.html
Component spec indexcomponent-specifications/index.html
UI Refresh roadmap (AB#1821)roadmap.html