Skip to content
HotLayout Docs

Android reference

The details behind the quickstart for Android: what the com.hotlayout:hotlayout-android library takes, what it does with documents, themes, data, actions, fonts, language, pictures and the network, what it reports, and how it decides whether it can read a document. The quickstart is the path from an empty app to a published screen; this page is the reference, ordered by topic. The README of the library is a short introduction.

What each schema minor added is in the contract changelog, and which library version reads which minor is in Which package reads which schema. What changed in each library version is on the releases of the library.

Related pages: quickstart for Android · widgets · compatibility · placements · localization · theme · data dictionary

The library#

Contract it reads document contract 9.34.0. Every widget of the contract is drawn, and the rules of the contract (versions, data, conditions, text, numbers, dates, plurals, locale, actions and navigation) pass the shared case tables
The rule the major version must match: a 9.x reader draws any 9.x document and no 8.x one. A document that uses a field newer than the reader still opens, and the reader skips the field. See Version compatibility
Requires Android 8.0 (API 26) and later, Jetpack Compose. No third-party dependencies besides AndroidX and Compose; the Compose compiler plugin is needed, as in any Compose app
Install Maven Central, com.hotlayout:hotlayout-android; the steps are in the quickstart
The package the whole public API is in the Kotlin package com.hotlayout
Permission the library does not declare INTERNET. The default picture loader downloads over the network, so an app that shows pictures from https: addresses adds <uses-permission android:name="android.permission.INTERNET" /> to its own manifest. An app that loads pictures through its own client (ImagePolicy.fetch) does not need it for the reader
Licence notices Apache License 2.0. The icon drawings include icons from Lucide (ISC); the notices are in THIRD-PARTY-NOTICES.md of the library: include them in your app's licences screen

Status: complete for contract 9.34.0. The drawn widgets include text and rich text, buttons, lists and list rows, stacks and layers, progress bars and rings, the form controls and text fields (with forms and their validation), the screen anatomy (app bar, body, bottom zone and navigation bar), modal layers with the system back key, carousels, node motion and glass, images, and the adaptive width classes, with the alignment rules on both axes.

The public API#

HotlayoutScreen the composable that draws a document — see Drawing a document
loadDocument, HotlayoutDocument, HotlayoutThemeSet.read, HotlayoutLoadException read a document or a theme; see Loading a document and a theme
checkDocumentVersion, VersionVerdict, HotlayoutVersions the version rule, without reading the whole document
resolveLocale, LocaleResolution which translation, number format and direction a device locale gets
HotlayoutAction, StandardAction, StandardActions.url, runStandardAction what reaches onAction, and the helpers for the standard actions
TrackEvent, ScreenShownEvent, NavigateRequest, ActionSource what the other callbacks receive
ImagePolicy, WritablePaths, HotlayoutFonts, HotlayoutLabels decisions of the host: pictures, writable data paths, fonts, texts of the reader
dataScopeOf, JsonValue the data you pass in
HotlayoutReport what the reader skipped or replaced; never shown to the person
HotlayoutPlacement, PlacementSource, loadPlacement, PlacementLoad, PlacementStore, PlacementTransport placements — see Placements

Drawing a document#

HotlayoutScreen(document = …) draws a document. The quickstart shows it with the usual callbacks; this table is the whole list.

Parameter What you pass
document (required) a HotlayoutDocument from loadDocument. Reading is work for a background thread
modifier the box the screen is drawn in; the width class of the screen comes from this box, not from the window
theme a HotlayoutThemeSet; HotlayoutThemeSet.builtIn without one
themeId the theme of the set to draw with; the set's default without it
data the values for the document's bindings, a flat map from a data path to a value; build it with dataScopeOf(…) — see Data
locale the device locale (ar-EG, pt_BR); the first locale of the configuration without it — see Language and right-to-left
entry a named entry point of the document. A name the document does not have starts on the initial screen
timeZone the zone dates are written in; the zone of the device without it
now "now" in epoch milliseconds for dates and counted days; without it the clock of the device, read once when the document is first drawn. With a moment given you own the clock: a time left does not count down on its own
images an ImagePolicy — see Images
fonts HotlayoutFonts: the families the theme names, from your resources; the system font otherwise — see Fonts
labels HotlayoutLabels: the few texts the reader says to assistive technology, in your language — see Accessibility and labels
onAction an action the reader does not perform itself, already checked, with an ActionSource — see Actions and navigation
onActionTask your answer to an action as a task (Job?): a swipe-to-confirm button stays busy until it ends
onTrack the analytics event of a press (TrackEvent), once per press, before the action
onHaptic the haptic word a press asks for; the reader never vibrates by itself
onNavigate called before the reader changes screens by itself; return false to cancel
onWrite, writablePaths what the user typed or chose, and which paths a document may write — see Writes
autofill false hands the platform no autofill hints from the text fields
handleSystemBack whether the reader takes the system back key — see The system back key
onScreenChange the top of the stack changed (the screen was shown, with the cause)
onReport what the reader skipped or replaced — see What the reader reports
safeAreaTop, safeAreaBottom insets you have already accounted for — see Window insets
onScreenCovered for a host with its own stack of screens — see Swipe fill

The reader keeps its own stack of screens, bound to the documentId and the entry, not to the document object: a host that reads the JSON again on every composition stays on the screen it is on. It performs navigation inside the document and draws only the document.

Loading a document and a theme#

val document by produceState<HotlayoutDocument?>(null, documentJson) {
    value = withContext(Dispatchers.Default) {
        try {
            loadDocument(documentJson)       // tolerant read + version gate
        } catch (error: HotlayoutLoadException) {
            null                              // show your own screen
        }
    }
}
val theme = remember(themeJson) { HotlayoutThemeSet.read(themeJson) ?: HotlayoutThemeSet.builtIn }

loadDocument throws HotlayoutLoadException on a document the reader cannot use: MalformedJson, NotADocument, MaxDepthExceeded or IncompatibleVersion (a newer major version). HotlayoutThemeSet.read returns null for a theme it cannot read: use HotlayoutThemeSet.builtIn, a theme that cannot be read is not a reason to refuse a screen. Where the document and the theme come from is your choice: your backend after a signed webhook, or the published address of the project. See Delivery options.

Version compatibility#

checkDocumentVersion applies the version rule without reading the whole document, for a host that wants to decide before it loads (and keep the old screen when the new document is too new). It takes JSON text, bytes or a JsonValue and returns a VersionVerdict: Ok, NewerMinor, NewerMajor, OlderMajor or Unparsable. The rule is the same in every reader and is described in the compatibility policy: the major must match; a newer minor opens and the reader skips what it does not know.

Library version Schema Platform
0.5.0 9.34.0 Android 8.0 (API 26)+, Jetpack Compose

Older library versions and what they read are in Which package reads which schema.

Data#

data maps paths to values, dataScopeOf("user.name" to "Maya", "user.points" to 1850). The rules are the same for every reader: a number stays a number, dates are ISO 8601 strings with time and zone, a missing value shows the fallback of the document, and a list draws at most 50 records. Which paths exist is defined by the project's data dictionary; what your backend returns is in the backend guide. The reader takes the values you pass; it never calls your backend.

Language and right-to-left#

locale defaults to the device locale. The reader picks the document's language by the contract's rule, formats numbers and dates for it and mirrors the layout for right-to-left scripts. resolveLocale tells which translation, number format and direction a locale gets. See Localization.

Actions and navigation#

  • Navigation inside a document (navigate, navigate.back, navigate.section) is done by the reader; onNavigate may cancel it by returning false. It is called after the haptic request and the analytics event of the same press, which are sent either way.
  • openUrl, deeplink, close and the actions of your app go to onAction. The reader opens nothing itself. A standard action arrives checked: runStandardAction(context, action) and StandardActions.url check it again before they open it (the form of the address, the scheme of a deeplink against the allowed ones, no invalid), so an action assembled by hand is not trusted because of its type.
  • A swipe-to-confirm button stays busy until the Job your onActionTask returns ends.

Swipe fill#

A swipe button with confirmTransition: 'ripple' that leads to another screen fills the screen with its colour before the screen changes. By default the fill lies over the HotlayoutScreen and the reader changes its own screens under it. If your app keeps its own stack of screens (you cancel the change with onNavigate and show your own view), pass onScreenCovered: the fill then lies in a layer over the whole window, above your views. The handler gets true when a fill starts to cover the screen, before the action reaches onNavigate or onAction (change your screen then, without a transition of your own), and false once the last fill is gone.

The system back key#

With handleSystemBack = true (the default) the reader takes the system back key (button, gesture, predictive back) as one step back of its own stack while the stack holds more than one screen and the top layer may be dismissed. With false, or at the first screen, the key goes to the host. A BackHandler the host registers after the reader's wins.

Writes#

Fields, setValue and toggleValue write to the session of the screen and to onWrite, already put into the session. writablePaths says which paths a document may write; by default every path of the form of a data path is allowed.

Images#

Images load only over https: by default. Pass an ImagePolicy as images to decide yourself. Its limits:

ImagePolicy
resolve your decision about an address
maxDownloadBytes 10 MB: a larger picture is drawn as the placeholder, announced or streamed
maxPixelSide 4096: a picture is decoded sampled down so its longest side is at most this, the ratio kept
totalDeadlineMs 30 s: the whole transfer, not only the silence between two reads, so a server that drips bytes is disconnected
fetch your own transport, suspend (address, maxBytes) -> ByteArray?, called with an address that has passed resolve. With it the reader makes no request of its own, so use it to give pictures a client with its own cookie jar, proxy and certificates. Return at most maxBytes; the call is held to totalDeadlineMs
  • The default loader sends and stores no cookies. HttpURLConnection cannot skip a global handler, so if your app has installed a java.net.CookieHandler (CookieHandler.getDefault() != null), the default loader refuses to fetch: the placeholder is drawn and the reason goes to onReport. Hosts with a global cookie handler pass fetch.
  • data: addresses never touch the network (the cap and maxPixelSide apply).

Fonts#

Fonts are yours: the theme names a family, HotlayoutFonts gives the FontFamily:

fonts = HotlayoutFonts { name -> if (name == "Inter") inter else null }

A family you do not give is drawn with the system font. If the family has no Arabic (or other) letters, the platform draws them with a system fallback font, chosen by the weight of the typeface. Static font files, one per weight, keep the weight. A variable font must be wrapped so that each weight is its own typeface, built with Typeface.Builder(...).setFontVariationSettings("'wght' N").setWeight(N) (an AndroidFont with a TypefaceLoader); Font(resId, weight, variationSettings = …) alone draws bold Arabic as regular.

Accessibility and labels#

The few texts the reader says to assistive technology (the name of the back arrow) are not in the document. Pass HotlayoutLabels(back = …) in the language of your app as labels; the defaults are English.

Window insets#

The reader adds the status bar, the cutout, the navigation bar and the keyboard itself, minus whatever an ancestor already consumed (safeDrawingPadding(), a Scaffold, consumeWindowInsets). The recommended host draws edge to edge (enableEdgeToEdge()) and lets the reader add them; a host that pads works as well and gets nothing added twice. safeAreaTop and safeAreaBottom still override the window's values.

Give the activity android:windowSoftInputMode="adjustResize" (with an edge-to-edge window it only reports the keyboard as an inset and does not resize anything). With the default the system may pan the whole window when the keyboard comes, and a field low on the screen is then shifted instead of scrolled above the keyboard.

What the reader reports#

onReport receives a list of HotlayoutReport: what the reader skipped or replaced while reading the document, the theme or the data, for your log or analytics. It is never shown to the person.

  • Documents and themes are untrusted input. A piece of the wrong shape is skipped and reported, never a crash.
  • Depth. A screen deeper than 32 levels is skipped (and reported); the other screens of the document still load.
  • Theme numbers. A number of a theme outside the bounds of the theme schema (or not finite) is replaced by the value of the same token in the built-in theme of the same brightness, otherwise by the nearest bound, and reported. Every length of a document is made finite, not negative and bounded.
  • Haptic feedback is asked of the host (onHaptic); the reader never vibrates.

The trust boundary and the tests that pin it are described in the security notes of the library.

Placements#

A placement is a named place of your app (home.banner). Which published document stands in it is decided in the admin, not in your code: you write the name once, and the screen changes without a release. The files and the rules of the map are in Placements; this part is what the Android reader does with them.

// Once, in your application object: one source serves every placement of the app.
val placements = PlacementSource(
    projectBase = "https://storage.example/orgs/acme/projects/app",
    bundledMap = context.assets.open("placements.json").bufferedReader().readText(),   // optional: the map of this release
)

@Composable
fun HomeBanner() {
    HotlayoutPlacement(
        name = "home.banner",
        source = placements,
        onAction = { action, _ -> /* as for HotlayoutScreen */ },
        onPlacement = { load -> if (load.report != null) Log.i("placement", "${load.name}: ${load.report}") },
    )
}

HotlayoutPlacement draws the document of the place with HotlayoutScreen (it takes the same parameters, and starts on the entry point the map names) and nothing when the place is empty, unknown, unreadable, or its document cannot be had. onPlacement hears every result with a PlacementLoad: report is Empty, Unknown, Unreadable, DocumentMissing, DocumentUnreadable, or EntryMissing (the entry point is gone from the document, which is drawn from its first screen), and null when all is well, so you can hide the frame around a place that draws nothing. Without Compose, loadPlacement(source, name) is the same as a suspend function; it runs off the main thread and never throws on a bad answer.

  • The map is placements/published.json under projectBase, read without a token and asked again every refreshIntervalMs (a minute by default, null for once; not below 5 seconds) with its ETag, so an unchanged map costs no body.
  • The network wins. When it is silent, broken, or of another major version, the last known map is used, then bundledMap. Only a fresh, readable map from the network becomes the last known one. A fresh map is the whole answer: a place it does not have is Unknown, it is not looked up in an older map.
  • PlacementLoad.mapStatus tells the cases apart: Offline (the network was silent), Corrupt (it answered with something that is not a map), Unsupported (another major), NotPublished (404: the project has no placements).
  • Documents go the same way: the network, the last known copy, then bundledDocuments. The last known map and documents live in memory; give PlacementStore a file or preferences to keep them over a restart.
  • A name that is not the name of a place is Unknown without a look at the map; a place of the map is found only by its own key. The map and the documents come from the network, so their ids are checked by form before they become an address.
  • The default transport is https only, follows no redirects, sends no credentials of its own (an app-wide CookieHandler does not stop it and may add cookies the app itself stored for the storage host), gives a request 20 seconds from start to the end of the body, and reads at most 256 KB (262 144 bytes) of map. Give PlacementTransport to use your own client. A map body must be UTF-8 (one leading byte order mark is skipped) and at most 32 levels deep, or it is Corrupt.
  • onPlacement is called for the first result and whenever the place changes: what is drawn, the report, the map status, the document or the entry point. A placement that comes back to a shown document is told too (report == null). The last known copies are kept under the address of their file, and a document that answered 404 is dropped from the store (PlacementStore.remove).

Schedule#

A place may carry a schedule (placement map 1.1.0): windows of time in which another document, or nothing, stands in it instead of its permanent content. Your code does not change: HotlayoutPlacement reads the place against the clock and swaps the document at the right moment, with no network at that moment.

  • An instant is UTC, whole seconds (2026-11-27T22:00:00Z); a window lasts from its from (included) to its until (excluded). A window with no document is a pause: the place is Empty for its length, and then the permanent content is back. When two windows overlap, the one that started later wins.
  • The clock is the device clock. Pass clock = { … } (milliseconds since the epoch) to PlacementSource to pin or shift time in tests. loadPlacement reads the place at that clock too.
  • HotlayoutPlacement sets one timer at the next boundary of the schedule and decides again there, on every new reading of the map, and when the app returns to the foreground (the clock may have jumped). The map is not asked again at a boundary. A broken window is skipped on its own; the place stays as it was.
  • onPlacement hears the swap like any other change (documentId / entry, and Empty with the document back). A place that is Empty in a pause is not an error: hide the frame around it and its own padding or margin, and keep HotlayoutPlacement itself in the composition, so the banner comes back on time:
var banner by remember { mutableStateOf(true) }
Column(Modifier.padding(horizontal = 16.dp)) {
    Box(Modifier.then(if (banner) Modifier.padding(bottom = 12.dp) else Modifier)) {
        HotlayoutPlacement("home.banner", placements, onPlacement = { banner = it.document != null })
    }
}

A reader older than 1.1.0 of the map shows the permanent content of a place and ignores its schedule.

Limitations#

  • Blur needs API 31. The blur under a modal layer and the glass of a node need a render effect, which exists from API 31 (Android 12). Below that the picture stays sharp and nothing is blurred: the page under a sheet is only dimmed and glass is drawn without blur.
  • Fonts are yours (see Fonts); the library ships none.
  • Where the Android platform and the shared case tables differ, and what the reader does, is in the platform notes of the library.

Next#