Instantly share code, notes, and snippets.
Created
November 29, 2025 06:46
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save webianks/e1b625613a5cd910b0312df6e007b363 to your computer and use it in GitHub Desktop.
PL MiniApp - StickyAdBanner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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