Skip to main content

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:

StepMultiplierValue at default
xs0.5×4 dp
sm1.0×8 dp
md1.5×12 dp
lg2.0×16 dp
xl3.0×24 dp
pillfully 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 (displayLargelabelSmall) 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:

StyleDefault size / line-heightWeight
displayLarge57 / 64Normal
displayMedium45 / 52Normal
displaySmall36 / 44Normal
headlineLarge32 / 40Normal
headlineMedium28 / 36Normal
headlineSmall24 / 32Normal
titleLarge22 / 28Normal
titleMedium16 / 24Medium
titleSmall14 / 20Medium
bodyLarge16 / 24Normal
bodyMedium14 / 20Normal
bodySmall12 / 16Normal
labelLarge14 / 20Medium
labelMedium12 / 16Medium
labelSmall11 / 16Medium

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 as ActivationIcon.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 Compose ImageVector.

Each form takes an optional tint (Long, 0xAARRGGBB) applied to the glyph.

The full icon catalogue:

IconWhere it appears
rewardIconNext to reward / point values throughout the SDK.
backIconNavigation icon (typically back / close) in the offers-wall app bar.
scanIconThe scan-receipt FAB on the offers wall.
moreStoresIconThe "more stores" affordance on the offers wall.
moreItemsIconThe "more items" affordance on the offers wall.
clipIconThe clip action when an offer is not yet clipped.
clippedIconThe clip action after an offer has been clipped.
defaultStoreIconFallback icon used when a store has no logo.
errorIconTop of the error modal.
claimIconThe "claim reward" button on the receipt summary.
missedEarningsIconNext to the missed-earnings callout on the receipt summary.
boostIconEach boost-offer card on the receipt summary.
niceScanIconThe "nice scan" celebratory banner on the receipt summary.
editMissedEarningEdit action on a missed-earnings row and in the missed-earnings top app bar.
submitMissedEarningsSubmit action in the missed-earnings top app bar.
addMissedEarningsThe "add" footer button on the missed-earnings list.
reportMissedEarningsThe "report" footer button on the missed-earnings list.
calendarMissedEarningsCalendar icon in the edit-date dialog.
submitMissedEarningsSuccessThe success snackbar shown after submitting missed earnings.
submitMissedEarningsErrorThe error snackbar shown after submitting missed earnings.
infoIconThe 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,
)
tip

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.