Skip to content

Instantly share code, notes, and snippets.

@webianks
Created November 29, 2025 06:46
Show Gist options
  • Select an option

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

Select an option

Save webianks/e1b625613a5cd910b0312df6e007b363 to your computer and use it in GitHub Desktop.
PL MiniApp - StickyAdBanner
package com.webianks.miniapps
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
// --- Data Models ---
data class Product(
val id: Int,
val name: String,
val price: Int,
val imageRes: Int // Changed from ProductType to image resource ID
)
// --- Hardcoded Data Source ---
object ProductRepository {
fun getProducts(): List<Product> {
// NOTE: Ensure you have added these images to your res/drawable folder
return listOf(
Product(1, "Google Pixel 9 Pro", 999, R.drawable.google_pixel_9_pro_2),
Product(2, "Google Pixel 9", 799, R.drawable.google_pixel_9_2),
Product(3, "Google Pixel 8 Pro", 899, R.drawable.google_pixel_8_pro),
Product(4, "Google Pixel 8", 699, R.drawable.google_pixel_8),
Product(5, "Google Pixel Fold", 1799, R.drawable.google_pixel_fold),
Product(6, "Google Pixel Tablet", 499, R.drawable.google_pixel_tablet),
Product(7, "Google Pixel Watch 2", 349, R.drawable.google_pixel_watch_2),
Product(8, "Google Pixel Buds Pro", 199, R.drawable.google_pixel_buds_pro),
Product(9, "Google Nest Hub (2nd Gen)", 99, R.drawable.google_nest_hub_2nd_gen),
Product(10, "Google Nest Audio", 99, R.drawable.google_nest_audio),
)
}
}
// --- Main Activity ---
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val view = LocalView.current
if (!view.isInEditMode) {
LaunchedEffect(Unit) {
val window = [email protected]
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
true
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars =
true
}
}
MiniAppsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface
) {
TechDealsApp()
}
}
}
}
}
// --- Composables ---
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TechDealsApp() {
Scaffold(
containerColor = Color.White,
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = "TechDeals",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
navigationIcon = {
IconButton(onClick = { /* No functionality required */ }) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = "Menu"
)
}
},
actions = {
IconButton(onClick = { /* No functionality required */ }) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(R.drawable.ic_cart),
contentDescription = "Cart"
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { innerPadding ->
ProductListScreen(modifier = Modifier.padding(innerPadding))
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProductListScreen(modifier: Modifier = Modifier) {
val products = ProductRepository.getProducts()
// State to manage banner visibility. rememberSaveable ensures it stays dismissed on config changes.
var isBannerVisible by rememberSaveable { mutableStateOf(true) }
val productsInRows = products.chunked(2)
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// 1. The first row
if (productsInRows.isNotEmpty()) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
productsInRows.first().forEach { product ->
Box(Modifier.weight(1f)) {
ProductItem(product)
}
}
}
}
}
// 2. The Sticky Banner
if (isBannerVisible) {
stickyHeader(
key = "sticky_banner",
contentType = "banner",
) {
Surface(color = Color.White) {
StickyPromoBanner(
onDismiss = { isBannerVisible = false }
)
}
}
}
// 3. The remaining items
items(productsInRows.drop(1)) { rowItems ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { product ->
Box(Modifier.weight(1f)) {
ProductItem(product)
}
}
if (rowItems.size < 2) {
Spacer(Modifier.weight(1f))
}
}
}
}
}
@Composable
fun StickyPromoBanner(
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxWidth()
.height(104.dp)
.clip(RoundedCornerShape(12.dp))
) {
Image(
painter = painterResource(id = R.drawable.header_bg),
contentDescription = "Promotional background",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Row(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = "Black Friday Deals",
color = Color.White.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "-50%",
color = Color.White,
style = MaterialTheme.typography.bodyMedium.copy(fontSize = 44.sp),
fontWeight = FontWeight.ExtraBold
)
}
}
// Close Button (Top Right)
IconButton(
onClick = onDismiss,
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Dismiss Banner",
tint = Color.White
)
}
}
}
@Composable
fun ProductItem(product: Product) {
Column(modifier = Modifier.fillMaxWidth()) {
Card(
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
),
modifier = Modifier.aspectRatio(1f)
) {
Image(
painter = painterResource(id = product.imageRes),
contentDescription = product.name,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = product.name,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
maxLines = 2
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "$${product.price}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
val White = Color(0xFFFFFFFF)
val TextPrimary = Color(0xFF041221)
val TextSecondary = Color(0xFF526881)
val Outline = Color(0xFFD8E4EA)
val HostGrotesk = FontFamily(
Font(R.font.host_grotesk_regular, FontWeight.Normal),
Font(R.font.host_grotesk_medium, FontWeight.Medium),
Font(R.font.host_grotesk_semibold, FontWeight.SemiBold),
Font(R.font.host_grotesk_bold, FontWeight.Bold)
)
// Set of Material typography styles to start with
val Typography = Typography(
titleLarge = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
lineHeight = 24.sp,
letterSpacing = 0.sp
),
bodyMedium = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = HostGrotesk,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
private val LightColorScheme = lightColorScheme(
primary = TextPrimary,
secondary = TextSecondary,
surface = White,
onPrimary = White,
onSecondary = White,
onSurface = TextPrimary,
outline = Outline
)
@Composable
fun MiniAppsTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = LightColorScheme,
typography = Typography,
content = content
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment