Starbucks (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports Starbucks’ visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, motion, and haptics.
Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Siren Green CTAs, the warm House Green dark canvas, the gold Stars dashboard ring, the Short→Trenta size selector) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar, adrawArcStars ring instead of a SwiftUItrim,sp/dpinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for the category-card photography. No color extraction — the Stars ring uses a fixed gold gradient brush, so androidx.palette is not required.
1. Color Tokens
// ui/theme/StarbucksColors.kt
import androidx.compose.ui.graphics.Color
object StarbucksColors {
// Canvas (Light)
val Canvas = Color(0xFFF9F9F9) // Warm White — softer than pure white
val CardSurface = Color(0xFFFFFFFF)
val SurfaceMuted = Color(0xFFF1F1F1)
val Divider = Color(0xFFE8E8E8)
// Text
val TextPrimary = Color(0xFF000000)
val TextSecondary = Color(0xFF595959)
val TextTertiary = Color(0xFF8E8E8E)
// Brand
val SirenGreen = Color(0xFF00704A)
val GreenPressed = Color(0xFF00563A)
val HouseGreen = Color(0xFF1E3932) // dark canvas — NOT black, deep forest green
val Mint = Color(0xFFD4E9E2)
// Gold (Rewards)
val GoldStar = Color(0xFFCBA258)
val GoldAlt = Color(0xFFC1A265)
val GoldLight = Color(0xFFE4C896)
// Semantic
val SuccessGreen = Color(0xFF00754A)
val AlertRed = Color(0xFFDD3333)
val InfoBlue = Color(0xFF006FCF)
val WarningAmber = Color(0xFFF5A623)
// Dark
val DarkCanvas = Color(0xFF1E3932) // House Green
val DarkSurface1 = Color(0xFF2D4A43)
val DarkSurface2 = Color(0xFF3B5852)
val DarkDivider = Color(0xFF3A4F48)
val DarkTextPrimary = Color(0xFFFFFFFF)
val DarkTextSecondary = Color(0xFFC9CDCB)
}
// The gold sheen used on the Stars ring stroke and the Scan-to-Pay header
val GoldRingBrush = androidx.compose.ui.graphics.Brush.horizontalGradient(
listOf(StarbucksColors.GoldStar, StarbucksColors.GoldLight, StarbucksColors.GoldStar)
)
Starbucks is a light-first brand (Warm White canvas), so wire a Material 3 lightColorScheme and supply a House-Green darkColorScheme for the night theme. Note onPrimary is intentionally pure white on Siren Green.
// ui/theme/Theme.kt
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.foundation.isSystemInDarkTheme
private val StarbucksLight = lightColorScheme(
primary = StarbucksColors.SirenGreen,
onPrimary = StarbucksColors.CardSurface,
secondary = StarbucksColors.Mint,
background = StarbucksColors.Canvas,
onBackground = StarbucksColors.TextPrimary,
surface = StarbucksColors.CardSurface,
onSurface = StarbucksColors.TextPrimary,
surfaceVariant = StarbucksColors.SurfaceMuted,
outline = StarbucksColors.Divider,
error = StarbucksColors.AlertRed,
)
private val StarbucksDark = darkColorScheme(
primary = StarbucksColors.SirenGreen,
onPrimary = StarbucksColors.CardSurface,
background = StarbucksColors.DarkCanvas, // House Green, not black
onBackground = StarbucksColors.DarkTextPrimary,
surface = StarbucksColors.DarkSurface1,
onSurface = StarbucksColors.DarkTextPrimary,
surfaceVariant = StarbucksColors.DarkSurface2,
outline = StarbucksColors.DarkDivider,
error = StarbucksColors.AlertRed,
)
@Composable
fun StarbucksTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) = MaterialTheme(
colorScheme = if (darkTheme) StarbucksDark else StarbucksLight,
typography = StarbucksTypography,
content = content,
)
2. Typography
SoDo Sans is proprietary (licensed from Dalton Maag, named after Seattle’s SoDo neighborhood). Drop the TTFs in res/font/ (lowercase, snake_case) and build a FontFamily. Fall back to the system font (Roboto) — its humanist tone is the closest free substitute on Android; for a closer match bundle Lato.
// ui/theme/StarbucksType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val SoDoSans = FontFamily(
Font(R.font.sodosans_regular, FontWeight.Normal), // 400
Font(R.font.sodosans_semibold, FontWeight.SemiBold), // 600
Font(R.font.sodosans_bold, FontWeight.Bold), // 700
Font(R.font.sodosans_extrabold, FontWeight.ExtraBold), // 800 — Stars + hero only
)
// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1, weights preserved)
object StarbucksText {
val StarsHero = TextStyle(SoDoSans, fontWeight = FontWeight.ExtraBold, fontSize = 48.sp, lineHeight = 48.sp, letterSpacing = (-0.5).sp)
val ScreenTitle = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.3).sp)
val SheetTitle = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
val Section = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 23.sp, letterSpacing = (-0.1).sp)
val CardTitle = TextStyle(SoDoSans, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 21.sp)
val DrinkTitle = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
val Body = TextStyle(SoDoSans, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
val Meta = TextStyle(SoDoSans, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
val Price = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 17.sp, lineHeight = 19.sp, letterSpacing = (-0.1).sp)
val StarsInline = TextStyle(SoDoSans, fontWeight = FontWeight.ExtraBold, fontSize = 22.sp, lineHeight = 22.sp)
val Button = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 16.sp, letterSpacing = 0.2.sp)
val ButtonSmall = TextStyle(SoDoSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 15.sp)
val SizePill = TextStyle(SoDoSans, fontWeight = FontWeight.SemiBold, fontSize = 13.sp, lineHeight = 13.sp, letterSpacing = 0.2.sp)
val Tab = TextStyle(SoDoSans, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 13.sp, letterSpacing = 0.1.sp)
val Chip = TextStyle(SoDoSans, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.3.sp)
}
// Map onto Material 3 slots so stock components inherit the brand
val StarbucksTypography = Typography(
headlineLarge = StarbucksText.ScreenTitle,
headlineSmall = StarbucksText.SheetTitle,
titleMedium = StarbucksText.CardTitle,
bodyMedium = StarbucksText.Body,
labelSmall = StarbucksText.Tab,
)
3. Signature Components
Primary CTA (Siren Green Pill)
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.foundation.clickable
import androidx.compose.ui.unit.dp
@Composable
fun StarbucksPrimaryButton(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (pressed) 0.98f else 1f,
animationSpec = spring(dampingRatio = 0.8f, stiffness = 600f),
label = "ctaScale",
)
val haptics = LocalHapticFeedback.current
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 52.dp)
.scale(scale)
// branded green glow — rgba(0,112,74,0.2) 0 4px 12px
.shadow(12.dp, RoundedCornerShape(50), spotColor = StarbucksColors.SirenGreen.copy(alpha = 0.2f))
.clip(RoundedCornerShape(50))
.background(if (pressed) StarbucksColors.GreenPressed else StarbucksColors.SirenGreen)
.clickable(interaction, indication = null) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress) // ~iOS medium impact
onClick()
}
.padding(horizontal = 24.dp, vertical = 16.dp),
contentAlignment = Alignment.Center,
) {
Text(title, style = StarbucksText.Button, color = StarbucksColors.CardSurface)
}
}
Category Card (Order Grid)
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage
@Composable
fun StarbucksCategoryCard(
title: String,
imageUrl: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
val scale by animateFloatAsState(if (pressed) 0.98f else 1f, spring(dampingRatio = 0.8f), label = "cardScale")
val haptics = LocalHapticFeedback.current
Box(
modifier = modifier
.aspectRatio(2f / 3f)
.scale(scale)
.shadow(8.dp, RoundedCornerShape(16.dp), spotColor = Color.Black.copy(alpha = 0.08f))
.clip(RoundedCornerShape(16.dp))
.clickable(interaction, indication = null) {
haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove) // ~iOS light impact
onClick()
},
contentAlignment = Alignment.BottomStart,
) {
AsyncImage(
model = imageUrl,
contentDescription = title,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
// bottom 40% darken for label legibility
Box(
Modifier
.fillMaxSize()
.background(Brush.verticalGradient(0.5f to Color.Transparent, 1f to Color.Black.copy(alpha = 0.5f)))
)
Text(
title,
style = StarbucksText.CardTitle,
color = StarbucksColors.CardSurface,
modifier = Modifier.padding(16.dp),
)
}
}
Syrup Pump Stepper
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Icon
@Composable
fun StarbucksPumpStepper(
label: String,
count: Int,
onCountChange: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = StarbucksText.ButtonSmall, color = StarbucksColors.TextPrimary)
Spacer(Modifier.weight(1f))
StepButton(Icons.Filled.Remove, "Decrease $label") { if (count > 0) onCountChange(count - 1) }
Text(
"$count",
style = StarbucksText.Price,
color = StarbucksColors.TextPrimary,
modifier = Modifier.widthIn(min = 44.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
)
StepButton(Icons.Filled.Add, "Increase $label") { onCountChange(count + 1) }
}
}
@Composable
private fun StepButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
contentDescription: String,
onClick: () -> Unit,
) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
val scale by animateFloatAsState(if (pressed) 0.9f else 1f, spring(stiffness = 700f), label = "stepScale")
val haptics = LocalHapticFeedback.current
Box(
modifier = Modifier
.size(36.dp)
.scale(scale)
.clip(CircleShape)
.border(1.5.dp, StarbucksColors.Divider, CircleShape)
.clickable(interaction, indication = null) {
haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onClick()
},
contentAlignment = Alignment.Center,
) {
Icon(icon, contentDescription, tint = StarbucksColors.TextPrimary, modifier = Modifier.size(14.dp))
}
}
Scan-to-Pay Barcode Card
import androidx.compose.material.icons.filled.Star
@Composable
fun StarbucksScanPayCard(
stars: Int,
balance: Double,
barcodeNumber: String, // "6241 1234 5678 9012"
barcode: @Composable () -> Unit, // supply a real Code128 renderer (e.g. ZXing bitmap)
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
// Stars summary — gold-gradient header
Row(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(GoldRingBrush)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Filled.Star, null, tint = StarbucksColors.CardSurface, modifier = Modifier.size(20.dp))
Spacer(Modifier.width(8.dp))
Text("$stars Stars", style = StarbucksText.StarsInline, color = StarbucksColors.CardSurface)
Spacer(Modifier.weight(1f))
Text("GOLD MEMBER", style = StarbucksText.Chip, color = StarbucksColors.CardSurface)
}
// Barcode
Column(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(StarbucksColors.CardSurface)
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(Modifier.height(80.dp), contentAlignment = Alignment.Center) { barcode() }
Text(barcodeNumber, style = StarbucksText.Meta.copy(letterSpacing = 2.sp), color = StarbucksColors.TextPrimary)
}
// Balance + Reload
Row(verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Balance", style = StarbucksText.Meta, color = StarbucksColors.TextSecondary)
Text("$%.2f".format(balance), style = StarbucksText.DrinkTitle, color = StarbucksColors.TextPrimary)
}
Spacer(Modifier.weight(1f))
StarbucksPrimaryButton("Reload", onClick = {}, modifier = Modifier.widthIn(max = 140.dp))
}
}
}
4. Stars Dashboard Ring + Size Selector (the signature dynamic system)
Starbucks does no color extraction. Its two defining interactions are the gold Stars progress ring that fills on appearance and the Short→Trenta size selector whose green pill slides between sizes. iOS draws the ring with a SwiftUI Circle().trim; in Compose use Canvas + drawArc with an Animatable sweep.
Stars Progress Ring
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@Composable
fun StarbucksStarsRing(
currentStars: Int,
targetStars: Int,
modifier: Modifier = Modifier,
) {
val target = (currentStars.toFloat() / targetStars).coerceIn(0f, 1f)
val progress = remember { Animatable(0f) }
LaunchedEffect(target) {
// 1.5s ease-out fill on appearance (DESIGN.md §6)
progress.animateTo(target, tween(durationMillis = 1500))
}
val remaining = (targetStars - currentStars).coerceAtLeast(0)
Column(
modifier = modifier.semantics {
contentDescription = "$currentStars Stars earned, $remaining more Stars until free reward"
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(contentAlignment = Alignment.Center) {
Canvas(Modifier.size(180.dp)) {
val stroke = 12.dp.toPx()
val inset = stroke / 2
val arcSize = Size(size.width - stroke, size.height - stroke)
// track
drawArc(
color = StarbucksColors.Divider,
startAngle = 0f, sweepAngle = 360f, useCenter = false,
topLeft = androidx.compose.ui.geometry.Offset(inset, inset),
size = arcSize,
style = Stroke(width = stroke),
)
// gold fill — starts at 12 o'clock (-90°), clockwise
drawArc(
brush = GoldRingBrush,
startAngle = -90f,
sweepAngle = 360f * progress.value,
useCenter = false,
topLeft = androidx.compose.ui.geometry.Offset(inset, inset),
size = arcSize,
style = Stroke(width = stroke, cap = StrokeCap.Round),
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("$currentStars", style = StarbucksText.StarsHero, color = StarbucksColors.GoldStar)
Text("Stars", style = StarbucksText.Meta.copy(fontWeight = FontWeight.SemiBold), color = StarbucksColors.TextSecondary)
}
}
Text("$remaining Stars until free reward", style = StarbucksText.Body, color = StarbucksColors.TextSecondary)
}
}
Size Selector (Short / Tall / Grande / Venti / Trenta)
import androidx.compose.material.icons.filled.Coffee
enum class CoffeeSize(val label: String, val cupScale: Float) {
SHORT("Short", 0.55f), TALL("Tall", 0.7f), GRANDE("Grande", 0.85f),
VENTI("Venti", 1.0f), TRENTA("Trenta", 1.15f),
}
@Composable
fun StarbucksSizeSelector(
selected: CoffeeSize,
onSelect: (CoffeeSize) -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CoffeeSize.entries.forEach { size ->
SizePill(size = size, isSelected = selected == size, onClick = { onSelect(size) })
}
}
}
@Composable
private fun SizePill(size: CoffeeSize, isSelected: Boolean, onClick: () -> Unit) {
val interaction = remember { MutableInteractionSource() }
val haptics = LocalHapticFeedback.current
// 250ms ease as the green background "slides" to the new size
val bg by animateColorAsState(
if (isSelected) StarbucksColors.SirenGreen else Color.Transparent,
tween(250), label = "pillBg",
)
val fg = if (isSelected) StarbucksColors.CardSurface else StarbucksColors.TextPrimary
Column(
modifier = Modifier
.heightIn(min = 36.dp)
.clip(RoundedCornerShape(50))
.background(bg)
.then(if (isSelected) Modifier else Modifier.border(1.5.dp, StarbucksColors.Divider, RoundedCornerShape(50)))
.clickable(interaction, indication = null) {
haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onClick()
}
.padding(horizontal = 14.dp, vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(Icons.Filled.Coffee, null, tint = fg, modifier = Modifier.size((20 * size.cupScale).dp))
Text(size.label, style = StarbucksText.SizePill, color = fg)
}
}
5. Bottom Navigation (Tab Bar)
Starbucks’ iOS tab bar is an opaque white bar (no blur) with Scan as the central in-store anchor. Use Material 3 NavigationBar; active tint is Siren Green (filled variant), inactive is #595959. Android has no live blur — but Starbucks doesn’t use one here anyway, so an opaque CardSurface container is exact.
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@Composable
fun StarbucksBottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(
containerColor = StarbucksColors.CardSurface,
tonalElevation = 0.dp,
) {
val items = listOf(
"Home" to Icons.Filled.Home,
"Scan" to Icons.Filled.QrCode, // the in-store anchor
"Order" to Icons.Filled.Coffee,
"Gift" to Icons.Filled.CardGiftcard,
"Offers" to Icons.Filled.LocalOffer,
)
items.forEachIndexed { i, (label, icon) ->
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(24.dp)) },
label = { Text(label, style = StarbucksText.Tab) },
alwaysShowLabel = true,
colors = NavigationBarItemDefaults.colors(
selectedIconColor = StarbucksColors.SirenGreen,
selectedTextColor = StarbucksColors.SirenGreen,
unselectedIconColor = StarbucksColors.TextSecondary,
unselectedTextColor = StarbucksColors.TextSecondary,
indicatorColor = StarbucksColors.Mint.copy(alpha = 0.4f),
),
)
}
}
}
Pair with a 0.5.dp top hairline (StarbucksColors.Divider) — render it as a HorizontalDivider above the bar inside the Scaffold bottomBar slot.
6. Motion
| Moment | Compose recipe |
|---|---|
| Stars ring fill | Animatable 0 → progress over tween(1500) ease-out, once on appearance |
| Size pill slide | animateColorAsState on the green fill with tween(250); HapticFeedbackType.TextHandleMove |
| Stepper press | animateFloatAsState 1 → 0.9 spring(stiffness=700f), light haptic, 80ms count bump |
| Category card tap | animateFloatAsState 1 → 0.98 spring(dampingRatio=0.8f), light haptic, then navigate |
| Stars earned tick-up | LaunchedEffect loop incrementing currentStars with 80ms delay; success haptic on last |
| Order placed | checkmark Animatable 0 → 1.2 → 1.0 spring(dampingRatio=0.55f); success haptic |
| Primary CTA press | animateFloatAsState 1 → 0.98 + color → GreenPressed; HapticFeedbackType.LongPress |
// Stars earned — tick the count up one Star at a time, then fire success haptic
@Composable
fun rememberStarsTicker(from: Int, to: Int): State<Int> {
val count = remember { mutableIntStateOf(from) }
val haptics = LocalHapticFeedback.current
LaunchedEffect(to) {
for (s in (from + 1)..to) {
kotlinx.coroutines.delay(80)
count.intValue = s
}
haptics.performHapticFeedback(HapticFeedbackType.LongPress) // success-like confirmation
}
return count
}
Haptics: prefer LocalHapticFeedback. For a richer “success” cue on order-placed, use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONFIRM) (API 30+) or a Vibrator VibrationEffect.createOneShot(12, ...) to approximate iOS’s medium impact.
7. Icons
Starbucks ships a few custom glyphs (the Siren, the cup silhouettes scaled by size). The closest first-party set is androidx.compose.material:material-icons-extended; for exact parity (the Siren store pin, the size-scaled cups) export them as vector drawables and load via ImageVector.vectorResource(R.drawable.…).
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Cup / size icon | cup.and.saucer.fill |
Icons.Filled.Coffee |
| Star (rewards) | star.fill |
Icons.Filled.Star |
| QR / Scan | qrcode |
Icons.Filled.QrCode |
| Plus / Minus (stepper) | plus / minus |
Icons.Filled.Add / Icons.Filled.Remove |
| Cart | cart.fill |
Icons.Filled.ShoppingCart |
| Store / pin | mappin.circle.fill |
Icons.Filled.Place |
| Home (tab) | house.fill |
Icons.Filled.Home |
| Order (tab) | cup.and.saucer.fill |
Icons.Filled.Coffee |
| Gift (tab) | gift.fill |
Icons.Filled.CardGiftcard |
| Offers (tab) | tag.fill |
Icons.Filled.LocalOffer |
| Checkmark (order placed) | checkmark.circle.fill |
Icons.Filled.CheckCircle |
| Info | info.circle |
Icons.Filled.Info |
| Clock (ready in) | clock.fill |
Icons.Filled.Schedule |
8. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21; the
Canvasring and motion are comfortable at 24).compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge()in theActivity. The Warm White canvas wants dark system-bar icons in light mode (WindowCompatlight status bar); House Green dark mode wants light icons. ApplyScaffoldinsets so the tab bar clears the gesture nav. - Font scaling:
sphonors the user’s font scale automatically — keep it on drink names, body, descriptions. Pin layout-sensitive text: the 48.sp Stars count (clamp at fixed size by deriving fromdpor wrapping inCompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f))), the 13.sp size-pill labels (pill widths are tight), and 11.sp tab labels. - TalkBack: the Stars ring exposes a single
contentDescription(“47 Stars earned, 75 more Stars until free reward”); each size pill announces “[Size] selected” / “[Size], double-tap to select” viaModifier.semantics; the barcode card announces the balance and that the barcode is displayed for scanning. - Touch targets: Material guidance is 48.dp minimum. The 52.dp CTA and 180.dp ring are clear; the 36.dp stepper/size pills have a 44–48.dp effective target via generous horizontal padding — bump the visual hit area with
Modifier.size(48.dp)if your build targets strict compliance. - Contrast: Gold
#CBA258on Warm White#F9F9F9passes WCAG AA only at the 22sp+ ExtraBold Stars treatment — for any 13–14sp gold text useGoldAlt #C1A265or darker.#595959secondary on#F9F9F9passes AA at 13sp+. - Reduce motion: detect
Settings.Global.ANIMATOR_DURATION_SCALE == 0f(or a stored user pref) and snap the Stars ring totargetimmediately and disable the size-pill slide. - Dynamic color: do not enable Material You
dynamicLightColorScheme()/dynamicDarkColorScheme()— Starbucks’ brand requires fixed Siren Green, the House Green dark canvas, and gold Stars regardless of wallpaper.