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

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.

  1. Add a GronaSlotView to the layout and name its placement with app: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.GronaSlotView
    android:id="@+id/homeTopSlot"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:gronaPlacement="home_top" />
    </LinearLayout>
  2. Set the slot’s screen when the screen starts. Setting it starts the slot.

    HomeFragment.kt
    import ai.grona.sdk.GronaScreen
    import ai.grona.sdk.GronaSlotView
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    view.findViewById<GronaSlotView>(R.id.homeTopSlot).screen = GronaScreen.HOME
    }
  3. 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.CART

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 GONE or INVISIBLE. 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.

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.

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.

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 screen starts 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 screen to null ends 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 = null
slot.screen = GronaScreen.CART

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.

  • 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’s assets/img/ folder, or as https URLs on hosts your configuration allows. Anything else is dropped and the component renders without it.

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.