Skip to content

Getting Started

This page is the whole integration: install, configure, load, show, style, observe, and clean up. The API Reference has the exhaustive detail on every type mentioned here.

Two Elo ad placements in a chat app: an in-chat card after the assistant message, and a below-input card under the composer

The SDK renders two placements: a card inline in the chat list (Show it) and a banner strip above the keyboard (Keyboard banner).

The SDK is available on Maven Central. Add it to your app’s build.gradle.kts:

build.gradle.kts
dependencies {
implementation("ad.elo:elo-ads-android:0.6.2")
}

Requires Android API 26+, Kotlin 2.0+, JVM target 11, and Compose BOM 2024.10.01+. The SDK ships Jetpack Compose ad views; XML-only apps host them inside a ComposeView — see Hosting from RecyclerView and XML.

Most public SDK types live in the root ad.elo.androidsdk package; Compose views live under ad.elo.androidsdk.ui:

import ad.elo.androidsdk.AdResult
import ad.elo.androidsdk.ChatMessage
import ad.elo.androidsdk.Elo
import ad.elo.androidsdk.EloError
import ad.elo.androidsdk.EloAdListener
import ad.elo.androidsdk.MessageRole
import ad.elo.androidsdk.ui.EloAdView
import ad.elo.androidsdk.ui.EloKeyboardBannerAd

Call this once, early in your app lifecycle (typically Application.onCreate()):

Elo.configure(
context = this,
publisherId = "YOUR_PUBLISHER_ID",
adUnitId = "YOUR_AD_UNIT_ID",
)

Good to know at this point:

  • Passive geo sharing is on by default. If your app already holds a location permission, the SDK attaches a coarse, rounded location to ad requests. It never prompts for the permission itself. Opt out with shareGeoLocation = false here or Elo.setShareGeoLocation(false) at runtime — see Privacy & Consent.
  • Child-directed apps must set the coppa / tfua flags — see Privacy & Consent.
  • COPPA/TFUA and log level go through the Elo.configure(context, EloConfiguration(...)) overload — see EloConfiguration.

Pass the recent chat history to get a contextual ad. loadAd is a suspend function — call it from a coroutine — and returns an AdResult:

val messages = listOf(
ChatMessage(MessageRole.USER, "Best laptops for coding?"),
ChatMessage(MessageRole.ASSISTANT, "I'd recommend the MacBook Pro M4..."),
)
lifecycleScope.launch {
val result = Elo.loadAd(messages)
}
  • Human-to-human chats use the same two roles: map the device owner to USER and the other participant to ASSISTANT — see MessageRole.
  • Long conversations: send a MessageRole.SUMMARY message followed by the most recent user/assistant exchange instead of the full history — either shape works.
  • Message ids and times: pass the id and createdAt (an Instant) your app keeps for each turn, the same values on every request, so Elo knows when each turn happened and can match the turn to a Search API call that sends the same id. A SUMMARY message carries the id and time of the turn it summarizes. See ChatMessage.
  • Extra targeting: pass contextObjects (screen/page metadata) when the surrounding UI carries useful signal.
  • Preloading: if a slot is about to become visible, warm the cache with fire-and-forget Elo.preloadAd(messages, maxHeight = maxHeight); a subsequent loadAd with identical inputs may return immediately. The cache key is the messages, the context objects, maxHeight and maxWidth, and the consent snapshot, so pass the same slot size to the preload and to the load that uses it.

EloAdView renders the ad and handles render, impression, and click tracking automatically (impressions fire after ≥50% visibility for 1 continuous second, while the app is resumed and the host window is focused and visible). Pass the AdResult and the view collapses to nothing on no-fill or error:

EloAdView(result = result)

For the common path, EloAdView can also own the request and loading state:

EloAdView(messages = messages)

That overload shows the built-in EloAdLoadingView skeleton while the request is in flight, then renders the ad on fill or collapses on no-fill. Pass showLoadingPlaceholder = false when the layout should reserve no ad space during loading. It starts a new request whenever messages or contextObjects changes — in chat feeds, pass a stable snapshot for the ad slot; if the conversation is still streaming or you need one request per assistant turn, call Elo.loadAd(...) yourself and render the returned AdResult.

In a LazyColumn chat list, inject ads as list items alongside messages:

ChatScreen.kt
sealed class ChatItem {
abstract val id: String
data class Message(
val role: MessageRole,
val content: String,
override val id: String = "msg-${UUID.randomUUID()}",
) : ChatItem()
data class Ad(val ad: EloAd) : ChatItem() {
override val id: String get() = "ad-${ad.id}"
}
}
@Composable
fun ChatList(items: List<ChatItem>) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) {
items(items, key = { it.id }) { item ->
when (item) {
is ChatItem.Message -> MessageBubble(item)
is ChatItem.Ad -> EloAdView(ad = item.ad)
}
}
}
}

Because EloAdView lives inside LazyColumn, the viewability tracker only fires the impression once the ad scrolls into view. Keys must be unique per item: EloAd.id identifies one showing, and each message gets its own id when it is created, because two messages can share the same text. Insert ads after assistant responses:

suspend fun handleAssistantReply(reply: ChatMessage) {
items += ChatItem.Message(reply.role, reply.content)
val history = items.filterIsInstance<ChatItem.Message>()
.map { ChatMessage(it.role, it.content) }
val result = Elo.loadAd(history)
if (result is AdResult.Loaded) {
items += ChatItem.Ad(result.ad)
}
}

Elo picks the ad’s format from the space you have. An ad between messages is a card. Pass maxHeight to a request-owning EloAdView for a smaller slot; see Ad Formats.

One composable at the screen level pins a two-line banner strip above the software keyboard — no IME tracking or inset code:

Box(Modifier.fillMaxSize()) {
ChatScreen()
EloKeyboardBannerAd(
messages = messages,
modifier = Modifier.align(Alignment.BottomCenter),
)
}

The slot requests the strip’s height, renders a strip on fill, and collapses on no-fill or error. When the keyboard is hidden it rests above the navigation bar. Changing messages reloads the slot; use the optional onResult callback to observe outcomes. While the request is in flight it shows a strip-shaped skeleton at the slot’s own size, so the composer above keeps its place when an ad arrives; pass showLoadingPlaceholder = false to keep the keyboard edge clear until an ad fills.

Three caveats:

  • The banner rides Compose’s WindowInsets.ime. Pinning above the keyboard requires an edge-to-edge window (enableEdgeToEdge()) with android:windowSoftInputMode="adjustResize"; in adjustPan or non-edge-to-edge windows it degrades to resting at the window bottom.
  • Place it where no ancestor has already consumed the IME insets (e.g. outside a Scaffold content area that applies imePadding()), or the strip will double-offset.
  • Keep your composer’s hit area from extending into the gap above the strip — ad networks (AdMob included) treat placements that invite accidental clicks next to text inputs as policy violations.

If your list is a RecyclerView, host each Compose ad view inside a ComposeView per row:

class AdViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
ComposeView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
},
) {
fun bind(ad: EloAd) {
(itemView as ComposeView).setContent {
EloAdView(ad = ad)
}
}
}

For a fixed placement in an XML screen, drop a ComposeView and call into the SDK’s @Composable from setContent:

<androidx.compose.ui.platform.ComposeView
android:id="@+id/adView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
lifecycleScope.launch {
val result = Elo.loadAd(messages)
binding.adView.setContent {
EloAdView(result = result)
}
}

By default, EloAdView inherits your Material 3 theme colors. Override any subset with EloAdStyle:

EloAdView(
result = result,
style = EloAdStyle(
cardBackground = Color(0xFF1A1A2E),
titleColor = Color.White,
descriptionColor = Color.LightGray,
borderColor = Color.Gray,
borderWidth = 1.dp,
cornerRadius = 16.dp,
),
)

Localize the disclosure badge per view by passing an EloAdDisclosure:

EloAdView(
result = result,
sponsoredLabel = EloAdDisclosure(stringResource(R.string.elo_sponsored)),
)

EloAdDisclosure also carries the badge’s fontSize, fontWeight, color, and a testTag for Compose UI tests.

The style also sets the typeface and can drop the ad’s own surface so it sits inside your chat row:

EloAdView(
result = result,
style = EloAdStyle(
callToActionBackground = Color(0xFFFFEB3B), // the button label turns black to stay readable
fontFamily = FontFamily(Font(R.font.inter)),
chrome = EloAdChrome.Bare,
),
)
  • fontFamily draws the headline, description, button label, and disclosure badge in your typeface. The SDK keeps its own sizes and weights, so the ad stays the height its slot asked for.
  • chrome = EloAdChrome.Bare removes the background, border, and rounded corners. The card also drops its inner padding, except 4dp at the top and end for the disclosure badge. The strip keeps its padding, because the padding holds its tap inset.
  • Set only callToActionBackground and the button label turns black or white, whichever contrasts more with your color.
  • The ad’s default colors come from MaterialTheme.colorScheme. To force light or dark colors on the ad alone, wrap it in your own theme: MaterialTheme(colorScheme = darkColorScheme()) { EloAdView(...) }.

All style properties are listed under EloAdStyle.

Register an EloAdListener to observe load, fail, impression, and click events. The SDK holds it weakly (retain it yourself, e.g. as a ViewModel property); all methods have default no-op implementations:

class ChatViewModel : ViewModel() {
private val adListener = object : EloAdListener {
override fun onAdDidLoad(ad: EloAd) { /* analytics */ }
override fun onAdDidFail(error: EloError) { /* logging */ }
override fun onAdDidTrackImpression(ad: EloAd) { /* analytics */ }
override fun onAdDidReceiveClick(ad: EloAd) { /* analytics */ }
}
fun startListening() {
Elo.setListener(adListener)
}
}

For a first-run QA check, keep the ad visible for at least one continuous second after it is 50% on screen: onAdDidLoad confirms the request filled, onAdDidTrackImpression confirms the viewability path fired, and onAdDidReceiveClick confirms click tracking.

If you open ad clicks in your own in-app browser, report when it opens and closes. The SDK sends how long the user stayed on the advertiser’s page.

override fun onAdDidReceiveClick(ad: EloAd) {
Elo.trackBrowserOpened(ad)
// Call when your browser closes.
browser.onClose = { Elo.trackBrowserClosed(ad) }
}

The time counts any time the app spent in the background, or the device spent asleep, while the browser was open. A second open before the close, and a close without an open, are ignored.

Elo is a single demand source. If you also run another ad network, keep the fallback in your own code: request Elo first, render the ad on Loaded, and hand the same slot to your other network on NoFill or Error. Both non-loaded cases mean Elo has nothing to show for this request, so treat them the same way.

LaunchedEffect(messages) {
slot = when (val result = Elo.loadAd(messages)) {
is AdResult.Loaded -> SlotState.Elo(result.ad) // e.g. EloAdView(ad = result.ad)
is AdResult.NoFill,
is AdResult.Error -> SlotState.OtherNetwork // your other network fills the same slot
}
}

In a chat UI you can request twice for one slot: once when the user’s message arrives, and again with the assistant’s answer appended if the first request did not fill. The second request carries more context, so it can fill where the first did not.

var result = Elo.loadAd(messages) // ends with the user's message
if (result !is AdResult.Loaded) {
result = Elo.loadAd(messages + answer)
}

AdResult.Loaded also carries eCpm, the price the Elo server quoted for the fill. When your other network quotes a price at load time, request both and render the higher one, calling EloAd.releaseResources() on the Elo ad you discard. Only call it when you’ve decided not to render the ad; ads passed to EloAdView are cleaned up by the SDK automatically. See Other Ad Networks for the comparison patterns.

  • Elo.disable() pauses ad requests without tearing down the SDK (e.g. while a paid user is signed in) — loadAd returns AdResult.NoFill(NoFillReason.SessionDisabled) until Elo.enable().
  • Elo.shutdown() cancels in-flight requests and closes the HTTP client; after it, loadAd returns AdResult.Error until you call configure(...) again.
  • Other Ad Networks — run Elo alongside another network with a fallback in your own code
  • API Reference — complete reference for all public types
  • Release Notes — what changed per version, with upgrade callouts
  • Privacy & Consent — CMP setup, COPPA/TFUA, geo sharing, and your Play Data safety disclosures
  • app-ads.txt — declare Elo as an authorized seller of your inventory