What You Will Build
A premium subscription screen with a feature checklist, selectable plan cards with radio buttons, and a subscribe button. Plans highlight with a border when selected. This is the standard monetization pattern used by apps offering tiered subscriptions.
Why This Pattern Matters
Subscription screens directly impact revenue. The key UX patterns here -- visual plan comparison, clear pricing, and feature checklists -- are backed by conversion optimization research. Learning to build selectable card groups with state management is essential for any commercial app.
Key Concepts
- mutableIntStateOf for tracking selected plan index.
- Card with conditional BorderStroke for selection highlighting.
- RadioButton synced with card selection state.
- forEachIndexed to render plan options dynamically.
The Subscription Screen
@Composable
fun SubscriptionScreenScreen() {
var selectedPlan by remember { mutableIntStateOf(1) }
val plans = listOf(
Triple("Monthly", "$9.99/mo", "Basic access"),
Triple("Yearly", "$79.99/yr", "Save 33%"),
Triple("Lifetime", "$199.99", "One-time")
)
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(32.dp))
Icon(Icons.Default.StarBorder, null,
modifier = Modifier.size(64.dp),
tint = Color(0xFFFFD700))
Text("Go Premium",
fontSize = 28.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(24.dp))
// Feature checklist
listOf(
"Unlimited access", "No ads",
"Priority support", "Exclusive content"
).forEach {
Row(
Modifier.fillMaxWidth().padding(vertical = 4.dp)
) {
Icon(Icons.Default.Check, null,
tint = Color(0xFF4CAF50))
Spacer(Modifier.width(8.dp))
Text(it)
}
}
Spacer(Modifier.height(24.dp))
// Plan cards with radio selection
plans.forEachIndexed { idx, (name, price, desc) ->
Card(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable { selectedPlan = idx },
border = if (idx == selectedPlan)
BorderStroke(2.dp, MaterialTheme.colorScheme.primary)
else null
) {
Row(
Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = idx == selectedPlan,
onClick = { selectedPlan = idx }
)
Column(Modifier.weight(1f)) {
Text(name, fontWeight = FontWeight.Bold)
Text(desc, fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Text(price,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary)
}
}
}
Spacer(Modifier.weight(1f))
Button(
onClick = { /* launch purchase flow */ },
modifier = Modifier.fillMaxWidth().height(50.dp)
) { Text("Subscribe Now") }
}
}
Tips and Pitfalls
- Default selection: Pre-selecting the middle plan (index 1) is a common conversion tactic that anchors users to the recommended tier.
- BorderStroke null trick: Passing null removes the border entirely rather than setting width to 0.
- mutableIntStateOf vs mutableStateOf(0): The former is a Compose optimization that avoids boxing the Int value.
- Billing integration: Connect the subscribe button to Google Play Billing Library's
BillingClient.launchBillingFlow().
Minimum SDK: API 21+ with Compose BOM