Android Theme Customization
The Activation SDK exposes two global styling hooks:
Activation.theme— every visual aspect of every SDK screen: brand palette, type scale, corner-radius unit, and every customisable icon. This page covers it in full.Activation.appearance— per-screen overrides layered on top of the theme: customisable text labels and fine-grained per-element color overrides. See Android Appearance Customization.
Theme sets the SDK-wide look; appearance overrides individual screens and elements. Set either once during app startup (or anytime later) and updates propagate live to every SDK screen currently composed:
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(primary = 0xFF7C3AED),
icons = ActivationTheme.Icons(
scanIcon = ActivationIcon.Bytes(myScanIconBytes),
),
)
You don't need to import Compose or wrap anything — the SDK handles composition and dark/light resolution internally.
Why Semantic Color Roles
The theme exposes a compact set of semantic color roles — primary, textAccent, surfaceAccent, and so on — rather than a per-element key for every button, label, and surface in the SDK. That's a deliberate design choice, and it's worth understanding why before you start branding.
The SDK is built on Material Design 3, all the way down. Every screen is Compose UI backed by a real Material 3 ColorScheme, and your tokens are bridged into the full set of M3 color roles internally. Material components — buttons, dialogs, snackbars, date pickers, text fields — derive their entire state space from those roles: pressed and focused states, ripples, disabled tints, selection highlights, elevation overlays. A per-element key could set a button's resting fill, but it couldn't tell Material what the ripple, the disabled state, or the focused outline should be — those are computed from the roles. Semantic roles let the SDK paint every interactive state correctly from the values you supply.
Roles keep a theme coherent — especially in dark mode. Every token exists in both a lightColors and a darkColors palette, and the SDK swaps between them with the system setting. A handful of semantic tokens gives you a coherent app in both modes from a small number of values, and lets the SDK guarantee accessibility pairs — for example, text on primary always contrasts primary — instead of leaving contrast to line up across dozens of independently set keys.
Quick Start
Minimal customization — set your brand color and every primary button, FAB, link, and accent surface across the SDK updates:
// Application.onCreate (or any time afterwards)
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(
primary = 0xFF7C3AED, // brand purple
textAccent = 0xFF6D28D9, // links / highlights
surfaceAccent = 0xFFF3E8FF, // soft tinted panels
),
)
Setting the Theme
The theme is settable from any thread, any time:
// Set once at startup
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(primary = 0xFF0062F2),
typography = ActivationTheme.Typography(sizeScaleFactor = 1.1f),
)
// Update later — propagates live
Activation.theme = Activation.theme.copy(
lightColors = Activation.theme.lightColors.copy(primary = 0xFFFF6B00),
)
Activation.theme recomposes every SDK screen that reads it when the value changes. There's no init() call, no reset, and no lifecycle to manage.
ActivationTheme — Global Palette
The theme defines four token families: lightColors/darkColors, shapes, typography, icons. All ship with sensible defaults — only override the ones you need:
data class ActivationTheme(
val lightColors: Colors = ActivationColorDefaults.lightColors(),
val darkColors: Colors = ActivationColorDefaults.darkColors(),
val shapes: Shapes = Shapes(),
val typography: Typography = Typography(),
val icons: Icons = Icons(),
)
ActivationColorDefaults.lightColors() and ActivationColorDefaults.darkColors() are factory functions that accept 20 named Long parameters, each with a default. Pass only the tokens you want to change:
// Override just the brand color in both palettes
ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(primary = 0xFF7C3AED),
darkColors = ActivationColorDefaults.darkColors(primary = 0xFF8B5CF6),
)
The SDK resolves which palette to use automatically — darkColors when the system is in dark mode, lightColors otherwise (see Dark Mode).
Colors
20 brand and semantic color tokens, each encoded as a Long (0xAARRGGBB). Defaults form the SDK's Cobalt brand palette. The defaults below are the light palette values.
Six tokens default from a sibling token rather than a fixed value — accent, surfaceBrand, textOnPrimary, textOnSecondary, tagSurface, and tagText. They're invisible until you override them, and exist to give you finer control (e.g. a white receipt-summary header while buttons stay your brand color).
These are semantic roles that cascade SDK-wide, not per-element keys. To override one specific element on one specific screen, use the per-element color overrides on Activation.appearance — see Android Appearance Customization.
primary
Type: Long · Default: 0xFF0062F2
Primary brand color — primary button backgrounds, the scan-receipt FAB, action icons, pagination indicators, static icons, and the progress bar.
accent
Type: Long · Default: primary
Emphasis glyphs and indicator accents — section arrows, the "more stores" icon, edit pencils, input cursors. The non-text sibling of textAccent: use this for glyph tints, textAccent for labels. Independently overridable from primary so, for example, section arrows can carry a different accent color than your primary buttons.
secondary
Type: Long · Default: 0xFF004EC2
Secondary button backgrounds and the total-points pill background on the receipt summary.
background
Type: Long · Default: 0xFFFFFFFF
App background — the bottom layer behind every screen.
surface
Type: Long · Default: 0xFFFCFBFA
Surface background — cards, modals, in-app notifications.
surfaceAccent
Type: Long · Default: 0xFFEBF2FE
Accent background — reward cards, icon backgrounds, soft highlight panels.
surfaceBrand
Type: Long · Default: primary
Full-bleed brand band surfaces — the receipt-summary header band (including the strip drawn behind the status bar) and the slab gradient tail behind it. Independently overridable from primary so, for example, the header can be white while buttons stay your brand color.
surfaceInverse
Type: Long · Default: 0xFF262626
Inverse surface — for elements that need to break out of the main palette.
textPrimary
Type: Long · Default: 0xFF142641
Primary text — main body copy and headings.
textSecondary
Type: Long · Default: 0xFF9CA3AF
Secondary text — descriptions, supporting copy, helper text.
textAccent
Type: Long · Default: 0xFF0062F2
Accent text — links, highlighted text, secondary emphasis.
textInverse
Type: Long · Default: 0xFFFFFFFF
Text painted on dark/inverse surfaces — snackbars, toasts, inverse panels (surfaceInverse). For text/icons sitting on primary- or secondary-filled controls, use textOnPrimary / textOnSecondary instead.
textOnPrimary
Type: Long · Default: textInverse
Text and icons sitting on primary-filled controls — buttons, the FAB, filled dialog actions.
textOnSecondary
Type: Long · Default: textInverse
Text and icons sitting on secondary-filled surfaces — e.g. the total-points pill text on the receipt summary.
success
Type: Long · Default: 0xFF29CC6A
Success state — clipped icons, check marks, success borders, positive feedback.
error
Type: Long · Default: 0xFFF43F5E
Error state — validation failures, destructive actions.
warning
Type: Long · Default: 0xFFFCA355
Warning state — non-blocking cautions, attention prompts.
border
Type: Long · Default: 0xFFE5E7EB
Border / structural divider — card borders, field borders, hairline separators.
tagSurface
Type: Long · Default: surfaceAccent
Promo tag pill background — e.g. the "BUY 2" badge on an offer card.
tagText
Type: Long · Default: textAccent
Promo tag text — e.g. the "BUY 2" label.
Shapes
Two tunable values control the entire corner-radius and border-width scale.
unit
Type: Float · Default: 8f (dp)
Base corner unit. Every corner radius in the SDK is a fixed multiple of this value:
| Step | Multiplier | Value at default |
|---|---|---|
xs | 0.5× | 4 dp |
sm | 1.0× | 8 dp |
md | 1.5× | 12 dp |
lg | 2.0× | 16 dp |
xl | 3.0× | 24 dp |
pill | fully rounded | — |
Set unit = 0f for sharp corners, unit = 12f for a softer feel — the whole scale follows.
borderWidthDp
Type: Float · Default: 1f (dp)
Width applied to card borders, field outlines, and hairline separators across the SDK.
Typography
The SDK ships a Material 3–compatible type ramp (displayLarge … labelSmall) and exposes two tuning knobs.
sizeScaleFactor
Type: Float · Default: 1.0f
Multiplier applied to every text style's font size and line-height. 1.0f = design defaults; 1.1f → 10% larger across the board; 0.9f shrinks the entire ramp.
fontFamily
Type: FontFamily? (from androidx.compose.ui.text.font) · Default: null
Font applied to every SDK text style. null falls back to the Android system font (Roboto).
Activation.theme = ActivationTheme(
typography = ActivationTheme.Typography(
fontFamily = FontFamily(ResourcesCompat.getFont(context, R.font.brand)),
),
)
fontFamily is the one place in the styling API where host code touches a Compose type directly — every other ActivationTheme field is a plain Kotlin primitive. Leave it null if you want to keep your host module Compose-free.
The full type ramp:
| Style | Default size / line-height | Weight |
|---|---|---|
displayLarge | 57 / 64 | Normal |
displayMedium | 45 / 52 | Normal |
displaySmall | 36 / 44 | Normal |
headlineLarge | 32 / 40 | Normal |
headlineMedium | 28 / 36 | Normal |
headlineSmall | 24 / 32 | Normal |
titleLarge | 22 / 28 | Normal |
titleMedium | 16 / 24 | Medium |
titleSmall | 14 / 20 | Medium |
bodyLarge | 16 / 24 | Normal |
bodyMedium | 14 / 20 | Normal |
bodySmall | 12 / 16 | Normal |
labelLarge | 14 / 20 | Medium |
labelMedium | 12 / 16 | Medium |
labelSmall | 11 / 16 | Medium |
Icons
Every customisable glyph in the SDK lives on a single flat ActivationTheme.Icons record. Each field is an ActivationIcon? defaulting to null, meaning "use the SDK's built-in default" — set only the ones you want to override.
data class Icons(
val rewardIcon: ActivationIcon? = null,
val backIcon: ActivationIcon? = null,
val scanIcon: ActivationIcon? = null,
val moreStoresIcon: ActivationIcon? = null,
val moreItemsIcon: ActivationIcon? = null,
val clipIcon: ActivationIcon? = null,
val clippedIcon: ActivationIcon? = null,
val defaultStoreIcon: ActivationIcon? = null,
val errorIcon: ActivationIcon? = null,
val claimIcon: ActivationIcon? = null,
val missedEarningsIcon: ActivationIcon? = null,
val boostIcon: ActivationIcon? = null,
val niceScanIcon: ActivationIcon? = null,
val editMissedEarning: ActivationIcon? = null,
val submitMissedEarnings: ActivationIcon? = null,
val addMissedEarnings: ActivationIcon? = null,
val reportMissedEarnings: ActivationIcon? = null,
val calendarMissedEarnings: ActivationIcon? = null,
val submitMissedEarningsSuccess: ActivationIcon? = null,
val submitMissedEarningsError: ActivationIcon? = null,
val infoIcon: ActivationIcon? = null,
)
Activation.theme = ActivationTheme(
icons = ActivationTheme.Icons(
rewardIcon = ActivationIcon.Bytes(rewardIconBytes),
scanIcon = ActivationIcon.Bytes(scanIconBytes),
),
)
Each icon accepts an ActivationIcon. Build one with the convenience factory or one of the three forms — pick whichever fits how your asset is loaded:
ActivationIcon.fromDrawable(context, resId, tint = null)— Android convenience factory: loads a drawable resource and wraps its PNG bytes asActivationIcon.Bytes. The most natural form on Android.ActivationIcon.Bytes(data, tint = null)— raw image bytes (e.g. an SVG or PNG loaded from assets). The cross-platform, Compose-free path.ActivationIcon.Url(url, tint = null)— a remote image URL.ActivationIcon.Vector(imageVector, tint = null)— a ComposeImageVector.
Each form takes an optional tint (Long, 0xAARRGGBB) applied to the glyph.
The full icon catalogue:
| Icon | Where it appears |
|---|---|
rewardIcon | Next to reward / point values throughout the SDK. |
backIcon | Navigation icon (typically back / close) in the offers-wall app bar. |
scanIcon | The scan-receipt FAB on the offers wall. |
moreStoresIcon | The "more stores" affordance on the offers wall. |
moreItemsIcon | The "more items" affordance on the offers wall. |
clipIcon | The clip action when an offer is not yet clipped. |
clippedIcon | The clip action after an offer has been clipped. |
defaultStoreIcon | Fallback icon used when a store has no logo. |
errorIcon | Top of the error modal. |
claimIcon | The "claim reward" button on the receipt summary. |
missedEarningsIcon | Next to the missed-earnings callout on the receipt summary. |
boostIcon | Each boost-offer card on the receipt summary. |
niceScanIcon | The "nice scan" celebratory banner on the receipt summary. |
editMissedEarning | Edit action on a missed-earnings row and in the missed-earnings top app bar. |
submitMissedEarnings | Submit action in the missed-earnings top app bar. |
addMissedEarnings | The "add" footer button on the missed-earnings list. |
reportMissedEarnings | The "report" footer button on the missed-earnings list. |
calendarMissedEarnings | Calendar icon in the edit-date dialog. |
submitMissedEarningsSuccess | The success snackbar shown after submitting missed earnings. |
submitMissedEarningsError | The error snackbar shown after submitting missed earnings. |
infoIcon | The tappable info icon next to a product item's caption on the receipt summary; tapping it reveals the product tooltip. |
Dark Mode (darkColors)
ActivationTheme ships a built-in darkColors palette by default, populated by ActivationColorDefaults.darkColors(). The SDK automatically uses darkColors when the system is in dark mode (isSystemInDarkTheme()) and lightColors otherwise — no configuration required for the default Cobalt dark theme.
Override individual tokens to adapt the dark palette to your brand:
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(
primary = 0xFF0062F2,
background = 0xFFFFFFFF,
),
darkColors = ActivationColorDefaults.darkColors(
primary = 0xFF3B82F6, // brightened for dark backgrounds
background = 0xFF0A1426, // deep blue surface
textPrimary = 0xFFF3F4F6,
textInverse = 0xFF142641,
),
)
Single Shared Palette
You don't have to maintain two palettes. If your brand uses one consistent look in both light and dark mode, build a single Colors set once and pass it to both lightColors and darkColors. The SDK then renders the same palette regardless of the system setting — isSystemInDarkTheme() no longer changes anything.
val customBrandingColors = ActivationColorDefaults.lightColors(
primary = 0xFF7C3AED,
secondary = 0xFF7C3AED,
textAccent = 0xFF6D28D9,
)
Activation.theme = ActivationTheme(
lightColors = customBrandingColors,
darkColors = customBrandingColors,
)
This is the simplest way to ship a brand palette: define your tokens once and reuse them. Either ActivationColorDefaults.lightColors(...) or darkColors(...) works to build the shared set — both return a Colors instance and differ only in the defaults for tokens you don't specify.
ActivationAppearance — Per-Screen Overrides
Activation.theme controls the SDK's global look; Activation.appearance layers per-screen overrides on top — both the wording shown on individual screens (e.g. the scan-receipt FAB label on the offers wall) and fine-grained per-element color overrides (e.g. just the offer-card background, or just the receipt-summary header band). Every field defaults to null: labels fall back to the SDK's bundled localized strings, and colors fall back to the exact theme token that element already renders with — so a host that sets nothing sees pixel-identical rendering.
Activation.appearance = ActivationAppearance(
offersWall = ActivationAppearance.OffersWall(
colors = ActivationAppearance.OffersWall.Colors(
offerWallBackground = 0xFFFFFFFF, // override just this screen's background
),
labels = ActivationAppearance.OffersWall.Labels(
scanExtendedLabel = "Scan your receipt",
),
),
)
Theme first, appearance second: set your brand roles here on the theme and the whole SDK follows; reach for appearance colors only when one specific element needs to differ from its role.
For the full per-screen catalogue — labels, the color-override model, per-element color tables for every screen, and worked examples — see Android Appearance Customization.
Putting It Together
A realistic configuration mixing colors, dark colors, shapes, typography, and icons:
// Application.onCreate
Activation.theme = ActivationTheme(
lightColors = ActivationColorDefaults.lightColors(
primary = 0xFF7C3AED,
secondary = 0xFF7C3AED,
textAccent = 0xFF6D28D9,
surfaceAccent = 0xFFF3E8FF,
),
darkColors = ActivationColorDefaults.darkColors(
primary = 0xFF8B5CF6,
background = 0xFF0F0A1A,
surface = 0xFF1A1130,
textPrimary = 0xFFEDE9FE,
textInverse = 0xFF1A1130,
),
shapes = ActivationTheme.Shapes(unit = 12f),
typography = ActivationTheme.Typography(sizeScaleFactor = 1.05f),
icons = ActivationTheme.Icons(
rewardIcon = ActivationIcon.Bytes(loadAsset("reward_glyph.svg")),
scanIcon = ActivationIcon.Bytes(loadAsset("scan_icon.svg")),
),
)
Result: the brand swaps from Cobalt to a custom purple with a softer dark-mode variant, slightly larger type, a 12 dp corner unit, and overridden reward / scan glyphs. To layer per-screen overrides on top — custom scan-FAB wording, a single screen's background, a white header band — see the combined example in Android Appearance Customization.
Best Practices
Set the theme once
Activation.theme is designed to be set during app startup. You can mutate it anytime — updates are live — but treating it as configuration rather than runtime state keeps the model simple.
Customise ActivationTheme.darkColors for your brand's dark palette
The SDK ships built-in dark defaults (ActivationColorDefaults.darkColors()) and swaps to them automatically under isSystemInDarkTheme(). Override individual tokens via ActivationColorDefaults.darkColors(primary = 0xFF...) to match your brand without specifying every token from scratch.
Encode Long colors as 0xAARRGGBB
Always include the alpha byte (0xFF… for fully opaque). The SDK expects the standard packed Android color encoding.
Test both schemes
When shipping a custom palette, render the offers wall and receipt summary in both light and dark mode. textOnPrimary / textOnSecondary / textInverse are white by default — if your primary, secondary, or surfaceInverse is light, white-on-light will fail contrast. Validate the primary/textOnPrimary, secondary/textOnSecondary, and tagSurface/tagText pairs in particular; each defaults from a sibling token, so overriding one without the other can silently break contrast.
FAQ
Do I have to import Compose to use this?
No. Activation.theme and Activation.appearance are plain Kotlin objects. Set them from Application.onCreate, a ViewModel, or anywhere else. The one exception is Typography.fontFamily, which is a Compose FontFamily type — leave it null if you want to keep your host module Compose-free.
What's the difference between Activation.theme and Activation.appearance?
theme sets ~20 semantic color roles (plus shapes, typography, icons) that cascade SDK-wide. appearance applies per-screen overrides on top — both text labels and fine-grained per-element color overrides. Rebrand with theme; reach for appearance when you need to change one specific element beyond what the shared roles allow. They're independent hooks; set either, both, or neither. See Android Appearance Customization.
What happens if I change Activation.theme after the SDK is on screen?
Every SDK screen currently composed recomposes with the new tokens. There's no flush, no reset, no lifecycle hook to call. The same applies to Activation.appearance.
Why are ActivationTheme.Colors fields Long instead of Color?
So host code can configure them without depending on Compose. Plain Long values can be passed across module boundaries and through DI containers without dragging in any UI framework types.
Why are theme icons nullable?
null means "use the SDK's built-in default." Only set a field when you want to override the default. The same convention applies to appearance labels and colors — see Android Appearance Customization.
Where do I put font overrides?
Set ActivationTheme.Typography.fontFamily directly with a Compose FontFamily, e.g. FontFamily(ResourcesCompat.getFont(context, R.font.inter)) or FontFamily(Font(R.font.inter)). Leaving it null keeps the Android system font (Roboto).
Is the SDK offer wall accessible (a11y)?
The internal SDK screens use platform-native semantics (Compose Modifier.semantics). The default theme passes WCAG AA contrast on key surfaces; if you customise heavily, validate contrast for primary / textOnPrimary, secondary / textOnSecondary, surfaceAccent / textAccent, and tagSurface / tagText pairs in particular.