Android guide
Show a TaleArt zone in an Android app with the Android SDK: a view for XML layouts, or a composable for Jetpack Compose. You need the zone key from the dashboard, the same key the Web SDK uses. The Android SDK sends analytics events too (see Analytics below), so the zone's Analytics tab counts the app along with the web.
Before you start
- Android 7.0 (API 24) or later, and Kotlin. The SDK brings its own Compose UI; a Compose app needs nothing more.
- For the XML view: a host with a lifecycle, such as a
ComponentActivity, anAppCompatActivityor a Fragment. A plainActivityshows nothing and logs a warning. - A zone with a display, and its key (
tak_…), from the zone's Install tab in the dashboard.
Steps
Add the dependency
Add
android-sdkto your app module. It comes from Maven Central, groupsite.lempert.taleart, and bringsmobile-sdk(the shared core) with it. Gradle Kotlin DSL:dependencies { implementation("site.lempert.taleart:android-sdk:0.1.1") }Groovy (
build.gradle):dependencies { implementation 'site.lempert.taleart:android-sdk:0.1.1' }Your app also needs the internet permission, which the SDK does not add for you. In
AndroidManifest.xml:<uses-permission android:name="android.permission.INTERNET" />Diagram of the dependency graph (not a screenshot). Get the zone key
On the zone's Install tab in the dashboard, copy the key (
tak_…). The key is shown once, when it is issued. Put it in your code as a value, never in a URL. It reads that one zone, so treat what the zone shows as public. Rotating the key issues a new one and stops the old one at once.Diagram of the Install tab (not a screenshot). Place the zone: XML or Compose
In XML, add a
TaleartViewwith the key inapp:taleartZoneKey. Its width fills the parent and its height iswrap_content: the bar or banner sets the height.<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="match_parent" android:orientation="vertical"> <site.lempert.taleart.sdk.android.TaleartView android:id="@+id/taleart_top" android:layout_width="match_parent" android:layout_height="wrap_content" app:taleartZoneKey="tak_YOUR_ZONE_KEY" /> </LinearLayout>In Compose, call
TaleartZone. It takes the API origin and the key first; an empty API origin uses the one set in step 4.import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.fillMaxWidth import site.lempert.taleart.sdk.ui.TaleartZone @Composable fun HomeScreen() { Column { TaleartZone( apiUrl = "", // empty: the API URL set in TaleartSdk.init zoneKey = "tak_YOUR_ZONE_KEY", modifier = Modifier.fillMaxWidth(), ) } }
The bar in the TaleArt sample app (taller) on Android, a real screenshot taken on an emulator. The sample app shows only the zone here (its taller.zoneOnlydebug option), so the picture holds nothing but the SDK's own output. The row scrolls sideways, so the fourth cover is cut off at the screen edge.Initialise once in Application
Set the SDK's configuration once, in
Application.onCreate, before any view shows. Every view reads it; a laterTaleartSdk.initreplaces it, and the views read it again. SetapiUrl: a native app has no page to take the origin from, and with an empty one a zone shows nothing. Register the class in the manifest:import android.app.Application import site.lempert.taleart.sdk.TaleartConfig import site.lempert.taleart.sdk.TaleartSdk class TaleartApp : Application() { override fun onCreate() { super.onCreate() TaleartSdk.init( TaleartConfig( apiUrl = "https://api.example.com", ), ) } }<application android:name=".TaleartApp" ... >Diagram of the start-up order (not a screenshot). Pass viewer properties
Pass what the app knows about the viewer as
properties: a map from a name to a String, a number, a Boolean or a list of Strings. A tale group can carry an audience rule, set in the dashboard under its targeting, and the SDK shows the group only when the rule passes these values. The rule can test a value witheq,in,gt,contains,existsand the other operators listed on the Targeting page.TaleartSdk.init( TaleartConfig( apiUrl = "https://api.example.com", properties = mapOf( "plan" to "pro", // a String "age" to 34, // a number "newsletter" to true, // a Boolean "segment" to listOf("beta", "newsletter"), // a list of Strings ), ), )A name starts with a letter and uses letters, digits,
_,.or-, up to 64 characters, and is case-sensitive. A map has at most 50 properties, a string at most 256 characters and a list at most 50 items. An invalid entry is ignored. Dates go as epoch milliseconds.Leave the properties out (
null, the default) when the app passes none: every group's audience rule then passes, as it does with no tags. An empty map means the app knows nothing about the viewer: a group whose rule needs a value is then left out, while the negative conditions (neq,notIn,notExists) pass.A group shows only when its tags and its audience rule both pass. The properties stay on the device: the SDK sends them in no request header, no URL and no analytics event. The rule itself is public, as the tag names are, so keep private content out of rule values.
A view can use its own properties in code (there is no XML attribute for them). They replace the SDK's for that view only:
// One view only: its properties replace the SDK's, and its zone is read again at once. view.properties = mapOf("plan" to "free") // Java: view.setProperties(map) // Back to the SDK's properties: view.properties = nullA later
TaleartSdk.initapplies its properties on the next read of a zone: a zone already on screen keeps the groups it shows until it reads again, as it does for tags. Setting a view'spropertiesreads that view's zone at once. The ComposeTaleartZonehas no properties of its own (see Not available natively yet).Listen to hooks and override a link
A listener gets the zone's events:
onLoaded,onLoadFailed,onPresented,onActionClickedand the rest. Set it inTaleartConfig.listenerfor every view, or on one view with itslistener; the view's runs first. Each method has a default that does nothing, so override only what you use.onActionClickedis the one hook with an answer. Returntruewhen your app routed the link, and the SDK does not open the URL. Returnfalseand the system browser opens it.import site.lempert.taleart.sdk.TaleartEvent import site.lempert.taleart.sdk.TaleartListener class AppHooks(private val router: AppRouter) : TaleartListener { override fun onLoaded(event: TaleartEvent) { // The zone with the key prefix event.zoneKeyPrefix is on screen. } override fun onActionClicked(event: TaleartEvent): Boolean { val url = event.url ?: return false if (!url.startsWith("https://shop.example.com/")) return false router.open(url) // your own navigation return true // handled: the SDK does not open the browser } } // In Application.onCreate, next to apiUrl: // TaleartConfig(apiUrl = "https://api.example.com", listener = AppHooks(router))The methods, their payloads and the events are in the Android SDK reference.
onGroupImpressionfires on native too, once per group per load.onComponentEventreports a countdown's end and a promo code's copy.onUserInteractedreports each answer the server accepts to a poll, a quiz, a rating or a reaction.Diagram of the hook order and the link decision (not a screenshot). Localise the texts
The SDK's texts (the viewer's buttons, the banner's controls, the names it announces) are English by default. Set
locale(BCP 47, such ashe-IL) and the texts per locale. A text is looked up under the full locale, then its language, thenstringsForEveryLocale, then English. A missing key shows English, never the key. With no locale set, the device's locale is used.TaleartSdk.init( TaleartConfig( apiUrl = "https://api.example.com", locale = "he-IL", strings = mapOf( "he" to mapOf( "viewer.close" to "סגור", "bar.seen" to "{name}, נצפה", ), ), ), ) // One view only: // TaleartZone(apiUrl = "", zoneKey = "tak_…", locale = "he-IL", strings = mapOf("he" to mapOf("viewer.close" to "סגור")))The key names are in the Android SDK reference. Each string key has its English default there.

A Hebrew group in the sample app's viewer (taller), a real screenshot taken on an emulator. The group name and the controls sit by right-to-left rules. The viewer's buttons are icons in this shot, so it does not show their localised labels. Verify
Run the app on a device or an emulator, then check, in order:
- The bar (or banner) appears where you placed the view.
- A tap on a circle opens the full-screen viewer, and Close (in your locale) returns to the bar.
- Logcat, filtered on
TaleArt, shows the warnings the SDK logs. A wrong key shows no zone and logs one failure;onLoadFailedgets the message.
When the API runs on your own computer, newer Android versions can ask once to allow access to nearby devices or the local network; allow it, then reload.

A tale open in the viewer, a real screenshot taken on an emulator. The viewer is running (the Pause button shows, not Play), and the first of the group's two progress bars is filling. 
The settings screen of the TaleArt sample app (taller), not an SDK screen. It is where the sample app takes its API URL and zone key. The key is masked after its first eight characters in this picture.
Show a message when a zone fails
By default a view that cannot read its zone shows nothing. Set fallback = FallbackMode.Message in TaleartConfig to show the translated "Stories could not be loaded." text with a "Try again" button, which reads the zone again. A view can show its own placeholder instead with fallbackView, and that wins over the message.
TaleartSdk.init(
TaleartConfig(
apiUrl = "https://api.example.com",
fallback = FallbackMode.Message, // the message and a retry button, for every view
),
)
// One view only: your own placeholder, which wins over the message.
view.fallbackView = myPlaceholder // Java: view.setFallbackView(myPlaceholder)
taller.zoneOnly debug option), so the message and the button are all there is. Cap how often a viewer sees a group
frequency hides a tale group after the viewer has seen enough of it. A view is one opening of the viewer on the group. The count stays on this device and is never sent to TaleArt. Groups over the cap leave the display, and when none are left nothing shows. The default is no cap.
TaleartSdk.init(
TaleartConfig(
apiUrl = "https://api.example.com",
frequency = FrequencyRule.Once, // hide a group after the viewer has opened it once
// frequency = FrequencyRule.Daily, // at most one view per 24 hours
// frequency = FrequencyRule.Weekly, // at most one view per 7 days
// frequency = FrequencyRule.Count(views = 3), // hide a group after 3 views (1 to 50)
),
)
taller.frequency in the sample app), a real screenshot taken on an emulator. 
Bar layouts
A bar zone draws its groups in one of four layouts, set by the bar's layout setting in the dashboard. Ring is the default. The other bar settings (shape, size, titles, alignment and gap) apply where the table says so, and the rest are ignored. While the zone is read, the bar shows a loading skeleton in the footprint it had last, so the page does not move when the groups arrive. The skeleton is announced as busy, and it stands still under reduced motion.
| Layout | Use | Ignored settings |
|---|---|---|
ring | The default. Each cover sits in a gradient ring with its title under it; the ring turns while a group is unseen and stays grey once it is seen. | None. |
chip | A pill per group: a round cover, its title, and a dot while the group is unseen (the seen pill is muted and has no dot). The pills wrap onto more rows. | shape. The size sets the cover (28, 32 or 40 dp). |
poster | A portrait card per group: the cover fills it, with the title and the tale count under a dark scrim, and a segment for each tale along the top. | shape and size. The card is 112 by 188 dp and has a fixed corner radius. |
stack | The groups overlap in one collapsed pill: up to four round covers and a +N bubble for the rest, with the count of unseen groups beside them. The pill opens the first unseen group. | shape, showTitles, alignment and gap. The size sets the covers (40, 48 or 56 dp). |

taller.barLayout debug option and shows only the zone; a real screenshot taken on an emulator. Its zone has six groups, and the row scrolls sideways. 



taller.zoneOnly and taller.autoOpenItem debug options. It is under 8 seconds. Banner variants
A banner zone shows its groups one at a time, or in one of five other variants: the frame's variant setting in the dashboard picks it. A missing or unknown value is the carousel. Each variant ignores the settings the table names, and the rest apply. The carousel has been restyled: each slide shows the group's name and a Watch stories button on a scrim, and its previous and next buttons are 48 dp, so they are easy to touch. Right to left, the line of slides, the turn of the coverflow and the fan of the deck, and the marquee run the other way, and the buttons sit on the other edge.
| Variant | Use | Ignored settings |
|---|---|---|
carousel | The default. One slide at a time, with the group’s name and a Watch stories button on a scrim, previous and next buttons, dots and autoplay. | None. |
coverflow | The current slide in front, its neighbours turned away at the sides, the far ones smaller and fainter. Swipe, or tap a neighbour. | None. |
peek | The current slide in the middle with the neighbours showing at the edges, like a pager with padding. | None. |
deck | The slides fanned out like a hand of cards. The front card opens its group; a tap on a card behind it brings that card forward. | None. |
sticky | A full-width bar on the card colour, pinned to the bottom of the space its host gives it and clear of the system’s bottom bar: previous and next buttons, the current slide’s round cover and name, and a dismiss button (all 44 dp, 48 dp to touch). Dismissed, it stays hidden until the app restarts. | aspectRatio, showDots and loop. It always loops. |
strip | A continuous marquee of the groups’ covers and names, scrolling at a fixed speed (about 18 seconds a loop) toward the start of the line. A press pauses it; under reduced motion it stops and can be scrolled sideways. | aspectRatio, showDots, loop, autoplay and intervalSeconds. |






Analytics events
The SDK sends impression, open, view and click events for each zone, in batches and in the background, so the app never waits on them. Turn them off for every view with analytics = false, or for one view with app:taleartAnalytics="false"; a view's setting wins. On native, an impression is a group's bar item first drawn, or a banner slide first shown: the SDK does not measure whether it is on screen.
TaleartSdk.init(
TaleartConfig(
apiUrl = "https://api.example.com",
analytics = true, // false turns the SDK's events off for every view
),
)<site.lempert.taleart.sdk.android.TaleartView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:taleartZoneKey="tak_YOUR_ZONE_KEY"
app:taleartAnalytics="false" />Countdowns and promo codes
A tale can hold a countdown: the days, hours, minutes and seconds left to a moment in UTC, then its ended text. A tale can also hold a promo code: the code on a ticket, with a Copy stub beside it. Add either in the dashboard, as you add any element of a tale; the SDK draws it as the web SDK does. The countdown counts on the device's clock, so a visitor with a wrong clock sees a wrong time. A promo code is visible to every visitor, so it is not a secret.
Countdown
Set the end as an instant in UTC, such as 2026-12-31T21:00:00Z. The units read Days, Hours, Min and Sec, and from the end on the ended text shows (Ended, unless you set your own). The end reaches your listener once, as onComponentEvent with event.action "ended" and the countdown as event.component.
Promo code
Set the code: 1 to 32 letters, digits, _ or -. Copy puts it on the clipboard and a toast over the card's top corner says Copied for four seconds; when the clipboard refuses, it says Could not copy. A copy reaches your listener as onComponentEvent with event.action "copied" and the code as event.component.value.
import site.lempert.taleart.sdk.TaleartEvent
import site.lempert.taleart.sdk.TaleartListener
// A countdown's end and a promo code's copy reach the listener as componentEvent.
class ComponentHooks : TaleartListener {
override fun onComponentEvent(event: TaleartEvent) {
when (event.action) {
// A countdown reached its end, once; event.component.type is "countdown".
"ended" -> println("Countdown ${event.component?.id} ended")
// A promo code was copied; the code is the component's value.
"copied" -> println("Copied ${event.component?.value}")
}
}
}
taller.autoOpenItem debug option. 


taller.autoCopy debug option, cropped above the foot of the screen where Android shows its own clipboard preview. Polls, quizzes, ratings and reactions
A tale can hold one poll, one quiz, one rating and one reaction, each added in the dashboard as any element of a tale. A visitor answers each once: the answer is kept on the device, so a tale opened again shows the result at once. The server counts the answers and the SDK draws the counts it returns. In a preview nothing is sent or kept.
Poll
Two options as buttons. A vote shows each option's share, with the vote selected, and the result is read to screen readers.


Poll views
A poll is drawn in one of five views, chosen by the poll's view. The sticker is the default, and a poll without a view, or with one the SDK does not know, is a sticker. The view changes how the poll looks, not what it records: a vote is counted the same way in every view. Two options can carry a colour (options[].color, #rrggbb); only this-or-that and grid use it, and without one the theme's colours are used.
| View | Use | Option colour |
|---|---|---|
sticker (default) | A small card over the tale, for a quick question. | Not used: the theme's colours. |
embed | A full-width card with its own results header: the vote total once voted, else the line that results appear after a vote. | Not used: the theme's colours. |
this-or-that | Two halves that fill the card. Once voted they split at the vote's share and a VS mark sits on the seam; mirrored in right to left. | Each half takes its option's colour, else the theme's; the text on it is white or black, whichever reads better. |
grid | Two large tiles side by side, each with its label and, once voted, its share. | Each tile takes its option's colour, else the theme's; the text on it is white or black, whichever reads better. |
inline | Compact rows, for a poll in the flow of a page. | Not used: the theme's colours. |
The embed, this-or-that, grid and inline views are shown before a vote and after one. The voted images are real votes in the sample app: a vote for Midnight in each view.








taller.autoOpenItem and taller.autoVote debug options. Quiz
Two to four answers. Once answered, the quiz says whether the answer was right: the correct answer is marked with a tick and a wrong one with a cross, as words too. Without a correct answer the quiz works as a survey and shows only the shares. The correct answer comes with the tale, so a quiz is not a secure test.


taller.autoVote debug option. Rating
A slider from 0 to 100 whose thumb is an emoji you choose. The visitor drags it or taps the track, and the rating is sent when they let go. Once rated, the count and the average show.


Reaction
Two to six different emoji. A tap sends that emoji; every emoji then shows its count, and the tapped one is selected.


taller.autoPress debug option. Each accepted answer reaches your listener as onUserInteracted, with the element in event.component and the answer as its value, and as onComponentEvent with event.action "selected". An answer kept from an earlier visit is not reported again. When an answer fails, the SDK says so on screen and the visitor can answer again.
Right to left
The SDK's views lay out in the device's direction: Hebrew and Arabic are right to left, so the bar starts on the right, a viewer's progress fills from the right, Previous is the right third of the tale, and the poll and rating layouts follow. Numbers, promo codes and a countdown's digits stay left to right. To set the direction yourself, pass layoutDirection = "rtl" or "ltr"; any other value, or none, follows the device.
TaleartSdk.init(
TaleartConfig(
apiUrl = "https://api.example.com",
locale = "he-IL",
layoutDirection = "rtl", // "ltr" or "rtl"; null (the default) follows the device
),
)
layoutDirection set to rtl, a real screenshot taken on an emulator. The first group is on the right. 
Backups and the visitor id
The SDK makes a visitor id once and keeps it in its own store on the device: the taleart-sdk SharedPreferences. It is random, not derived from the person or the device, and tied to no account. It is still a stable identifier of an app install, so cover it in your privacy notice and your consent setup. Clearing the app's data makes a new one. A reinstall does too, unless a device backup restores the store, and Android Auto Backup does restore it. To start fresh after a reinstall, exclude sharedpref/taleart-sdk.xml in the app's backup rules: android:fullBackupContent for Android 11 and lower, and android:dataExtractionRules from Android 12.
<full-backup-content>
<exclude domain="sharedpref" path="taleart-sdk.xml" />
</full-backup-content><data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="taleart-sdk.xml" />
</cloud-backup>
</data-extraction-rules>Not available natively yet
- onGroupImpression counts a group when it is first shown in a render, not when it is on screen: the native SDK does not measure visibility yet.
- The Compose TaleartZone has no properties parameter: a composable uses the SDK’s viewer properties from TaleartConfig, and cannot set its own for one zone.
The Install a zone page covers the Web SDK, which draws the same elements.