Render Grona components
This page places Grona’s own components in your screens. You add a GronaSlotView at each placement once, ship the release, and from then on Grona’s configuration decides what fills it, for whom and against which control group. If you want to draw the content with your own product views instead, see Host-rendered decisions.
A slot renders four component types: banner, progress, offer and message. For what each one looks like, see Grona components.
Add a slot
Section titled “Add a slot”-
Add a
GronaSlotViewto the layout and name its placement withapp:gronaPlacement:res/layout/fragment_home.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:paddingStart="16dp"android:paddingEnd="16dp"><ai.grona.sdk.GronaSlotViewandroid:id="@+id/homeTopSlot"android:layout_width="match_parent"android:layout_height="wrap_content"app:gronaPlacement="home_top" /></LinearLayout> -
Set the slot’s
screenwhen the screen starts. Setting it starts the slot.HomeFragment.kt import ai.grona.sdk.GronaScreenimport ai.grona.sdk.GronaSlotViewoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)view.findViewById<GronaSlotView>(R.id.homeTopSlot).screen = GronaScreen.HOME} -
Run the app. With nothing to show, the slot is laid out at zero height. When a campaign targets the placement, its component appears in the slot.
To create a slot in code, set placement before screen, because setting screen is what binds the slot:
val slot = GronaSlotView(context).apply { placement = "cart_top"}container.addView(slot, ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT))slot.screen = GronaScreen.CARTKeep the slot visible
Section titled “Keep the slot visible”A slot with nothing to show stays VISIBLE at zero height, so the control arm is measured on the same view as the treatment arm. Grona logs anchor_visible for every arm once the slot’s top edge has been on screen for one second.
- Never set a slot to
GONEorINVISIBLE. A hidden slot is never measured, so only the treatment arm would be counted. Debuggable builds log a warning once per hidden slot. - Put spacing inside the slot’s parent, as padding, not as margins on the slot. Margins on an empty slot still show as a gap.
- Padding or a minimum height on the slot itself does not stop it collapsing to zero height when empty.
For why this matters, see Events and exposure.
Placements
Section titled “Placements”The placement set is fixed. Use these names exactly, in XML or in code.
| Placement | Where it goes |
|---|---|
home_top |
Top of the home screen |
home_feed |
In the home feed |
plp_inline |
Inline in a product listing |
pdp_below_buy |
On the product page, below the buy button |
cart_line |
On a cart line. Keyed by the line’s product id, and the only placement for substitute decisions |
cart_top |
Top of the cart |
cart_below_lines |
In the cart, below the lines |
checkout_above_pay |
On checkout, above the pay button |
order_confirmation |
On the order confirmation screen |
membership_top |
Top of the membership screen |
A configuration that names any other placement is rejected for that campaign. For guidance on choosing placements, see Placements.
Screens
Section titled “Screens”GronaScreen tells Grona where the shopper is. Rules read it through screen.type, screen.product, screen.category and screen.treatments.
| Value | Use it for |
|---|---|
GronaScreen.HOME |
Home |
GronaScreen.CATEGORIES |
The category list |
GronaScreen.SEARCH |
Search results |
GronaScreen.CART |
The cart |
GronaScreen.CHECKOUT |
Checkout |
GronaScreen.category(categoryId) |
One category’s product listing, for example GronaScreen.category("dairy") |
GronaScreen.product(productId) |
One product page, for example GronaScreen.product("sku_10432") |
GronaScreen.orderConfirmation(treatmentTypes) |
The confirmation screen. Pass the treatment types your order backend actually applied, not the ones the app showed |
GronaScreen.other(name) |
Any other screen, by your own name, for example GronaScreen.other("membership") |
Two screens are equal when their type and values are equal, so GronaScreen.product("sku_10432") set twice is the same screen.
Screen visits
Section titled “Screen visits”Each placement on a screen counts as one visit. Grona logs eligible, anchor_visible, exposure and served at most once per visit, which keeps counts honest when views rebind.
- Setting
screenstarts a visit. Setting an equal value again continues it. - A slot that leaves its window and comes back keeps its visit, however long it was away. A new slot view, such as one a fragment creates again when it returns from the back stack, joins the visit only within 2 seconds after the last slot for it left.
- Setting
screentonullends the visit and empties the slot. - The slot drops its subscription with the view.
- Switching a campaign off applies at once, including to content on screen. Every other change, including a campaign 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 null and back, or cancel the decision’s subscription and request again. Each new visit logs eligible again.
To start a fresh visit on the same view, set screen to null and then to the screen again:
slot.screen = nullslot.screen = GronaScreen.CARTSlots in a RecyclerView
Section titled “Slots in a RecyclerView”A slot can sit in a RecyclerView row, such as a promotional row in a product listing. Set placement and screen in onBindViewHolder.
class PromoRowHolder(val slot: GronaSlotView) : RecyclerView.ViewHolder(slot)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val slot = GronaSlotView(parent.context).apply { layoutParams = RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT) } return PromoRowHolder(slot)}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is PromoRowHolder) { holder.slot.placement = "plp_inline" holder.slot.screen = GronaScreen.category(categoryId) }}The visit is keyed on placement, screen and key, not on the view. Binding the same placement and screen in another holder continues the visit while any slot for it is attached, and for 2 seconds after the last one detached. So a row that rebinds across holders logs its events once. A row that comes back in a different holder more than 2 seconds after it left starts a new visit. A slot that leaves the window and comes back keeps its visit.
Visibility and limits
Section titled “Visibility and limits”- A slot counts as live while it is attached, shown and in a visible window. A slot in a stopped activity or a hidden fragment keeps its content but is not live.
- Your workspace configuration caps the number of components on one screen. Lower-priority campaigns yield when the cap is reached. Only live slots count toward it.
- Images come from your configuration as
asset://img/...paths, which load from your app’sassets/img/folder, or as https URLs on hosts your configuration allows. Anything else is dropped and the component renders without it.
Taps and actions
Section titled “Taps and actions”A banner’s call to action can add products to the cart or open a screen. The SDK runs it only from the shopper’s tap, validates it, and then calls your GronaHost. See Handle actions.
Next steps
Section titled “Next steps”- Handle actions from Grona components.
- Match components to your app.
- Render decisions with your own views.