Skip to content
The mobile SDKs are available on request. Email support@grona.ai to get access.

Patterns for host-rendered decisions

With host-rendered decisions, your app draws the UI, so your app also decides how the screen behaves while a decision is pending, when there is none, and when one arrives late. This page gives the patterns that keep measurement fair and the layout stable. Use it alongside the platform pages for Android and iOS, which show the code.

The running example is a frequently-bought-together rail on pdp_below_buy: a rail decision with a title and three products, drawn with your own product cells.

Keep the anchor visible when the decision is null

Section titled “Keep the anchor visible when the decision is null”

The anchor is the view you pass to the decision call: the container your rail will occupy. The SDK measures visibility on it for every arm, including control and holdout, which never receive content. That is how both arms get the same anchor_visible measurement.

When the decision is null, leave the anchor in the layout, visible, at zero height, with no children. Its width does not matter. When a decision arrives, fill the anchor and let it grow.

A zero-height anchor stops sampling once anchor_visible has fired and resumes if it gains height, so empty control anchors cost nothing per frame.

Call the decision call on the main thread. The SDK delivers the decision through your listener, on the main thread, and never synchronously inside the call itself. So your screen always draws first without the decision, and the decision arrives a moment later, or not at all.

The listener can fire more than once for the same subscription: whenever re-evaluation changes the result, for example after a basket or profile change, and with null when the campaign is killed. An unchanged result is not delivered again. Treat every call as the current state and redraw from it.

The subscription lasts while the anchor view exists, including while it is off screen, and ends once your app releases the view. You can also end it yourself by cancelling it, for example when the screen closes.

Because the decision is never there on first draw, plan for content appearing below or around the shopper’s current scroll position.

  • Place the anchor where growth is harmless. Below the fold, below the add-to-basket button, or at the end of a list, a growing anchor pushes nothing the shopper is reading.
  • Reserve space only when the placement is always filled for this shopper. A fixed-height placeholder in an anchor that then receives null leaves a gap. Reserving space for control shoppers also makes the two arms look different.
  • Animate the growth. Expand the anchor from zero height rather than snapping it open.
  • Do not scroll the shopper. If content arrives above the visible area, keep the shopper’s position anchored to what they were looking at.

Keep spacing inside the anchor’s parent or inside the content you draw, not as margins on the anchor itself, so an empty anchor takes no space.

In a scrolling list, such as basket lines with cart_line, bind the decision per row and pass the row’s product id as the key. Bind each time a row is bound to a view. Binding the same anchor again replaces its previous subscription, so recycled rows can rebind freely.

When the shopper scrolls and a row moves to a different view, the same placement, screen and key on the new view continues the same screen visit. You do not get a second eligible, anchor_visible, served or exposure for that row.

Use the key, not the row position. Positions change when lines are added or removed; product ids do not.

A screen visit is identified by the placement, the screen and the key, not by the view. A view that leaves the screen and comes back keeps its visit. Another view that binds the same triple joins the visit while a view for it is still attached, or within 2 seconds after the last one detached or was replaced. That grace period covers list recycling and brief detaches during layout.

A new visit starts:

  • when a view binds the triple more than 2 seconds after the last view for it detached;
  • when your app cancels the subscription;
  • when a slot view’s screen is set to null.

Events such as eligible and served fire once per visit. Kills apply at once, even to content already on screen. Every other change, including a campaign that starts or is switched back on, waits for the next visit.

A screen that stays alive, such as a tab’s root screen, keeps its visit, so a campaign that starts or is switched back on later appears there only after the visit ends. To show changes, end the visit when the screen reappears: set screen to nil (Android: null) and back, or cancel the decision’s subscription and request again. Each new visit logs eligible again. To show a re-enabled campaign at once after a refresh, do the same from the refresh listener.

Decision titles, subtitles, badges, call-to-action labels and product labels are plain text. Render them without HTML, link detection or markdown.

Treat extras as untrusted layout hints. Validate each value against what your layout accepts, such as a known set of positions, and ignore anything else.

If a decision has a type your app does not render, treat it as no decision and keep the anchor at zero height.

When the shopper taps your add button, call the decision’s add, add-all or replace action, not your basket code directly. The SDK validates the products and quantities, calls your app’s add or replace, and records the action event with the result. Call dismiss when the shopper closes the content; it hides the decision for the rest of the session. See handle actions for Android or iOS.