Skip to content

Instantly share code, notes, and snippets.

@webianks
Created November 27, 2025 06:13
Show Gist options
  • Select an option

  • Save webianks/1d116035608ceebc4ef7174890ff424e to your computer and use it in GitHub Desktop.

Select an option

Save webianks/1d116035608ceebc4ef7174890ff424e to your computer and use it in GitHub Desktop.
package com.webianks.miniapps
import android.graphics.Color as AndroidColor
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
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.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
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.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// Typography
val HostGrotesk = FontFamily(
Font(R.font.host_grotesk_medium, FontWeight.Medium),
Font(R.font.host_grotesk_semibold, FontWeight.SemiBold)
)
val TechStoreTypography = Typography(
titleLarge = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
),
titleSmall = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 20.sp,
letterSpacing = 0.sp
),
bodyMedium = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp
),
labelLarge = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp
),
labelMedium = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Medium,
fontSize = 10.sp,
lineHeight = 14.sp,
letterSpacing = 0.5.sp
)
)
// Models
data class ProductSpecs(
val display: String,
val mainCam: String,
val frontCam: String,
val processor: String,
val ram: String,
val storage: String,
val battery: String
)
data class Product(
val id: Int,
val name: String,
val price: Int,
val salePrice: Int? = null,
val specs: ProductSpecs,
@DrawableRes val imageRes: Int
)
// Data
val PRODUCTS = listOf(
Product(
1, "Google Pixel 9", 799, null,
ProductSpecs("6.3\"", "50MP", "12MP", "Tensor G4", "8GB", "128GB", "4600mAh"),
R.drawable.google_pixel_9
),
Product(
2, "Samsung Galaxy S24+", 999, 899,
ProductSpecs("6.7\"", "50MP", "12MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "4900mAh"),
R.drawable.samsung_galaxy_s24
),
Product(
3, "iPhone 15 Pro", 1099, null,
ProductSpecs("6.1\"", "48MP", "12MP", "A17 Pro", "8GB", "256GB", "4350mAh"),
R.drawable.iphone_15_pro
),
Product(
4, "Google Pixel 9 Pro", 999, null,
ProductSpecs("6.7\"", "50MP", "12MP", "Tensor G4", "12GB", "256GB", "5100mAh"),
R.drawable.google_pixel_9_pro
),
Product(
5, "OnePlus 12", 899, 799,
ProductSpecs("6.8\"", "50MP", "32MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "5400mAh"),
R.drawable.oneplus_12
),
Product(
6, "Xiaomi 14", 799, 699,
ProductSpecs("6.4\"", "50MP", "32MP", "Snapdragon 8 Gen 3", "12GB", "256GB", "4610mAh"),
R.drawable.xiaomi_14
)
)
// Screen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(
AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT
),
navigationBarStyle = SystemBarStyle.light(
AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT
)
)
setContent {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF4291FF), // Brand
background = Color(0xFFEDF0F6),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF041221), // Text-Primary
onSurfaceVariant = Color(0xFF526881), // Text-Secondary & Text-Disabled
outline = Color(0xFFD8E4EA),
error = Color(0xFFE6556F) // Discount
),
typography = TechStoreTypography
) {
TechStoreApp()
}
}
}
}
// State and Navigation
enum class Screen {
ProductList,
Comparison
}
@Composable
fun TechStoreApp() {
var currentScreen by remember { mutableStateOf(Screen.ProductList) }
var selectedIds by remember { mutableStateOf(listOf<Int>()) }
val haptics = LocalHapticFeedback.current
fun toggleSelection(productId: Int) {
if (selectedIds.contains(productId)) {
selectedIds = selectedIds - productId
} else {
when (selectedIds.size) {
0, 1 -> {
selectedIds = selectedIds + productId
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
}
2 -> {
val first = selectedIds[0]
selectedIds = listOf(first, productId)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
}
}
fun clearSelection() {
selectedIds = emptyList()
}
Scaffold(
topBar = {
TopBar(
currentScreen = currentScreen,
selectionCount = selectedIds.size,
onBack = {
if (currentScreen == Screen.Comparison) {
currentScreen = Screen.ProductList
} else {
clearSelection()
}
},
onCompare = { currentScreen = Screen.Comparison }
)
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (currentScreen) {
Screen.ProductList -> {
ProductListScreen(
products = PRODUCTS,
selectedIds = selectedIds,
onProductLongPress = { toggleSelection(it) },
onProductClick = { id ->
if (selectedIds.contains(id)) {
toggleSelection(id)
}
}
)
}
Screen.Comparison -> {
val p1 = PRODUCTS.find { it.id == selectedIds.getOrNull(0) }
val p2 = PRODUCTS.find { it.id == selectedIds.getOrNull(1) }
if (p1 != null && p2 != null) {
ComparisonScreen(p1, p2)
} else {
// Fallback should rarely happen
Text("Error: Invalid selection")
}
// Handle hardware back button in Comparison view
BackHandler {
currentScreen = Screen.ProductList
}
}
}
}
}
}
// Components
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopBar(
currentScreen: Screen,
selectionCount: Int,
onBack: () -> Unit,
onCompare: () -> Unit
) {
val isSelectionMode = selectionCount > 0
val isComparison = currentScreen == Screen.Comparison
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background
),
title = {
if (isComparison) {
Text("Comparison", style = MaterialTheme.typography.titleLarge)
} else if (isSelectionMode) {
Text(
"$selectionCount item${if (selectionCount != 1) "s" else ""} selected",
style = MaterialTheme.typography.titleSmall,
color = Color(0xFFA8B7BF)
)
} else {
Text("TechStore", style = MaterialTheme.typography.titleLarge)
}
},
navigationIcon = {
if (isComparison || isSelectionMode) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
} else {
IconButton(onClick = { /* Menu Action */ }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu")
}
}
},
actions = {
if (isComparison) {
Spacer(modifier = Modifier.width(48.dp))
} else if (isSelectionMode) {
val canCompare = selectionCount >= 2
IconButton(
onClick = onCompare,
enabled = canCompare
) {
Icon(
painter = painterResource(R.drawable.ic_scale),
contentDescription = "Compare",
tint = if (canCompare) MaterialTheme.colorScheme.onSurface else Color.LightGray
)
}
} else {
IconButton(onClick = { /* Cart Action */ }) {
Icon(
painterResource(R.drawable.ic_cart),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = "Cart"
)
}
}
}
)
}
@Composable
fun ProductListScreen(
products: List<Product>,
selectedIds: List<Int>,
onProductLongPress: (Int) -> Unit,
onProductClick: (Int) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
items(products) { product ->
ProductItem(
product = product,
isSelected = selectedIds.contains(product.id),
onLongPress = { onProductLongPress(product.id) },
onClick = { onProductClick(product.id) }
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProductItem(
product: Product,
isSelected: Boolean,
onLongPress: () -> Unit,
onClick: () -> Unit
) {
val borderColor =
if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
val borderWidth = if (isSelected) 2.dp else 0.dp
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
modifier = Modifier
.fillMaxWidth()
.dropShadow(
shape = RoundedCornerShape(12.dp),
shadow = Shadow(
radius = 4.dp,
spread = 0.dp,
offset = DpOffset(0.dp, 4.dp),
alpha = 0.04f,
color = MaterialTheme.colorScheme.onSurface
)
)
.border(borderWidth, borderColor, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp))
.combinedClickable(
onClick = onClick,
onLongClick = onLongPress
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
) {
Image(
painter = painterResource(id = product.imageRes),
contentDescription = product.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit
)
// Sale Badge
if (product.salePrice != null) {
Badge(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(4.dp),
containerColor = MaterialTheme.colorScheme.error,
contentColor = Color.White
) {
Text(
"-38%",
style = MaterialTheme.typography.labelSmall,
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = product.name,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
// Price Row
Row(verticalAlignment = Alignment.Bottom) {
if (product.salePrice != null) {
Text(
text = "$${product.salePrice}",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "$${product.price}",
textDecoration = TextDecoration.LineThrough,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 2.dp)
)
} else {
Text(
text = "$${product.price}",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { /* No-op */ },
modifier = Modifier
.fillMaxWidth()
.height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = CircleShape
) {
Text("Buy", style = MaterialTheme.typography.bodyMedium)
}
}
}
}
@Composable
fun ComparisonScreen(p1: Product, p2: Product) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ComparisonHeaderItem(product = p1, modifier = Modifier.weight(1f))
ComparisonHeaderItem(product = p2, modifier = Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
// Specs Table
SpecRow("Display", p1.specs.display, p2.specs.display)
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
SpecRow("Main Camera", p1.specs.mainCam, p2.specs.mainCam)
SpecRow("Front Camera", p1.specs.frontCam, p2.specs.frontCam)
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
SpecRow("Processor", p1.specs.processor, p2.specs.processor)
SpecRow("RAM", p1.specs.ram, p2.specs.ram)
SpecRow("Storage", p1.specs.storage, p2.specs.storage)
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
SpecRow("Battery", p1.specs.battery, p2.specs.battery)
HorizontalDivider()
}
}
@Composable
fun ComparisonHeaderItem(product: Product, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.Start
) {
Box(
modifier = Modifier
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(12.dp))
) {
Image(
painter = painterResource(id = product.imageRes),
contentDescription = product.name,
modifier = Modifier
.fillMaxWidth()
.height(184.dp)
.padding(8.dp),
contentScale = ContentScale.Fit
)
// Sale Badge
if (product.salePrice != null) {
Badge(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(4.dp),
containerColor = MaterialTheme.colorScheme.error,
contentColor = Color.White
) {
Text(
"-38%",
style = MaterialTheme.typography.labelSmall,
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = product.name,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
if (product.salePrice != null) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "$${product.salePrice}",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "$${product.price}",
textDecoration = TextDecoration.LineThrough,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 2.dp)
)
}
} else {
Text(
text = "$${product.price}",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { },
modifier = Modifier
.fillMaxWidth()
.height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = CircleShape
) {
Text("Buy", style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
fun SpecRow(label: String, val1: String, val2: String) {
Column(modifier = Modifier.padding(vertical = 12.dp)) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 4.dp)
)
Row(modifier = Modifier.fillMaxWidth()) {
Text(
text = val1,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = val2,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment