Static Online Shopping App in Jetpack Compose

Flip Kat : Static Online Shopping App
A static app has fixed content that does not change unless manually updated by the developer. Displays the same content for all users.

Characteristics:

  • No real-time updates or database interaction.
  • Faster and more lightweight.
  • Lower development and maintenance costs.

Examples:

  • A simple calculator app.
  • An offline dictionary.
  • A brochure or portfolio app.

How to build Flip Kat a static online shopping app using Jetpack Compose:

What do we need:

1. Screens: Product List Screen, Product Details Screen, Cart Screen, shop view model and Payment Screen. 
2. Dependency: Navigation and View Model. 
3. Images: Images of products and App Icon

Dependency Needed

//  Coil for optimized image loading
implementation("io.coil-kt:coil-compose:2.5.0")

// Navigation Component for Compose
implementation ("androidx.navigation:navigation-compose:2.8.7")

// ViewModel for State Management
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")

Main Activity

Loading..

Product List Screen:

As we are going to build a simple app we will escape the authentication screen for login, sign up and log out. 
  We will create a screen to show products in grid.
rememberNavController() creates a NavController, which is used to navigate between screens.
NavHost defines different screens:
"productList" → Displays the product list.
"productDetail/{id}" → Displays details of a product based on the ID.
"cartScreen" → Displays the cart.
"paymentScreen" → Displays the payment screen.
The productDetail/{id} route uses dynamic arguments (id).
When navigating to a product detail page:

Scaffold Layout:

Uses Scaffold to provide a structure with a top app bar.
Inside Scaffold, a Column contains:
TopAppBar: Displays the app name.
TextField: Implements a search bar.

Search Bar Functionality

TextField binds to viewModel.searchQuery (which tracks the search input).
onValueChange = { viewModel.updateSearchQuery(it) } updates the query dynamically.
Product Display using LazyVerticalGrid

Uses LazyVerticalGrid to show products in a 2-column grid.
Displays filtered products (viewModel.filteredProducts).
Each product is displayed using ProductItem().

Card Layout

Each product is displayed inside a Card.
fillMaxWidth() makes it expand to the full available width.
height(480.dp) ensures a fixed size for all product items.
Uses painterResource(id = product.imageRes) to load product images.
contentScale = ContentScale.Crop ensures proper image scaling.
Displays the product name and price using Material 3 typography.
Checks if the product is already in the cart:
If yes, navigates to the cart.
If no, adds the product to the cart.
package com.codingbihar.flipkat

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun ShoppingNavHost(viewModel: ShopViewModel) {
val navController = rememberNavController()

NavHost(navController, startDestination = "productList") {
composable("productList") { ProductListScreen(viewModel, navController) }
composable("productDetail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")?.toIntOrNull()
if (id != null) ProductDetailScreen(id, viewModel, navController)
}
composable("cartScreen") { CartScreen(viewModel, navController) }
composable("paymentScreen") { PaymentScreen(viewModel) }
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProductListScreen(viewModel: ShopViewModel, navController: NavController) {
Scaffold(
topBar = {
Column {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) }
)

// Search Bar
TextField(
value = viewModel.searchQuery,
onValueChange = { viewModel.updateSearchQuery(it) },
placeholder = { Text("Search products...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search Icon") },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
}
}
) { innerPadding ->
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.padding(innerPadding)
) {
items(viewModel.filteredProducts) { product -> // Use filtered list
ProductItem(product, viewModel, navController)
}
}
}
}

@Composable
fun ProductItem(product: Product, viewModel: ShopViewModel, navController: NavController) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(480.dp)
.padding(8.dp)
) {
Box(Modifier.fillMaxSize()) {
// Using painterResource() correctly for drawable images
Image(
painter = painterResource(id = product.imageRes),
contentDescription = "Product Image",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)

Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Cyan)
) {
Column(
Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(product.name, style = MaterialTheme.typography.headlineMedium)
Text("${product.price}", style = MaterialTheme.typography.headlineSmall)

// Optimized Add to Cart Button
Button(onClick = {
if (viewModel.cart.any { it.id == product.id }) {
navController.navigate("cartScreen")
} else {
viewModel.addToCart(product)
}
}) {
Text(if (viewModel.cart.any { it.id == product.id }) "Go to Cart" else "Add to Cart")
}

Button(
onClick = {
navController.navigate("productDetail/${product.id}")
},
modifier = Modifier.fillMaxWidth()
) {
Text("Details")
}
}
}
}
}
}
}
Product Screen Shopping App in Jetpack Compose
Static Online Shopping App in Jetpack Compose

Product Detail Screen

  • @Composable: This function is a Composable, meaning it is part of the Jetpack Compose UI.
  • productId: Int: The ID of the product being displayed.
  • viewModel: ShopViewModel: Manages the product list and cart operations.
  • navController: NavController: Handles navigation between screens.
  • Searches the viewModel.productList for the product with the given productId.
  • If the product is not found, return exits the function to prevent a crash.
  • Scaffold: Provides a structure with a top app bar.
  • TopAppBar: Displays the product name as the title.
  • Uses painterResource(id = product.imageRes) to load the product image from resources.
  • fillMaxWidth() ensures the image stretches across the screen width.
  • height(250.dp) sets a fixed height for the image.
  • Displays the product price, description, diet information, and benefits.
  • Uses Material 3 typography styles (titleMedium, headlineMedium, headlineSmall).

Add to Cart Button

  • Checks if the product is already in the cart:
  • If yes, navigates to the cart screen.
  • If no, adds the product to the cart.
  • Button text dynamically updates between "Add to Cart" and "Go to Cart".
package com.codingbihar.flipkat

import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProductDetailScreen(productId: Int, viewModel: ShopViewModel, navController: NavController) {
val product = viewModel.productList.find { it.id == productId } ?: return

Scaffold(topBar = { TopAppBar(title = { Text(product.name) }) }) {
Column(modifier = Modifier.padding(16.dp)) {
// Display Product Image
Image(
painter = painterResource(id = product.imageRes),
contentDescription = "Product Image",
modifier = Modifier
.fillMaxWidth()
.height(250.dp) // Adjust image size
)

Spacer(modifier = Modifier.height(16.dp))

Text("${product.price}", style = MaterialTheme.typography.titleMedium)
Text(product.description, style = MaterialTheme.typography.headlineMedium)
Text(product.diet, style = MaterialTheme.typography.headlineSmall)
Text(product.benefits, style = MaterialTheme.typography.headlineSmall)

Spacer(modifier = Modifier.height(16.dp))

// Add to Cart Button
Button(onClick = {
if (viewModel.cart.any { it.id == product.id }) {
navController.navigate("cartScreen")
} else {
viewModel.addToCart(product)
}
}) {
Text(if (viewModel.cart.any { it.id == product.id }) "Go to Cart" else "Add to Cart")
}

}
}
}


Cart Screen

Uses LazyColumn to display cart items efficiently.
Calculates total price dynamically using sumOf on viewModel.cart.
Aligns total price to the right for better UI.
Proceed to Checkout Button navigates to paymentScreen.
Displays product image with a fixed size.
Shows product name & quantity in a well-structured Column.
Provides + and - buttons to adjust quantity.
Decrease button is disabled if quantity is 1.
Formats total price ("%.2f".format(price * quantity)) for correct decimal display.
Includes Remove Button (viewModel.removeFromCart(product))

addToCart(product)

If product exists in the cart, it increases quantity.
Otherwise, adds a new product with quantity = 1.
increaseQuantity(product)
Finds the product and increases its quantity.
decreaseQuantity(product)
Reduces quantity, but prevents it from going below 1.
removeFromCart(product)
Completely removes the product from the cart.
package com.codingbihar.flipkat

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController

@Composable
fun CartScreen(viewModel: ShopViewModel, navController: NavController) {
Column(
modifier = Modifier.fillMaxSize()
.systemBarsPadding(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Cart Screen", style = MaterialTheme.typography.headlineLarge)
LazyColumn (modifier = Modifier.weight(1f)) {
items(viewModel.cart) { product ->
CartItem(product, viewModel)
}
}

Text(
text = "Total: ${"%.2f".format(viewModel.cart.sumOf { it.price * it.quantity })}",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(end = 16.dp).align(Alignment.End))
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { navController.navigate("paymentScreen") }) {
Text("Proceed to Checkout")
}
}
}

@Composable
fun CartItem(product: Product, viewModel: ShopViewModel) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Left Side: Product Image
Image(
painter = painterResource(id = product.imageRes),
contentDescription = "Product Image",
modifier = Modifier
.size(80.dp) // Set image size
.padding(8.dp)
)

Spacer(modifier = Modifier.width(8.dp))

// Middle: Product Name & Quantity Selector
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(product.name, style = MaterialTheme.typography.headlineMedium, color = Color.Red)
Text("Quantity: ${product.quantity}", style = MaterialTheme.typography.titleMedium)
Row(
modifier = Modifier.fillMaxWidth(),
Arrangement.End
) {
TextButton(onClick = { viewModel.decreaseQuantity(product) }) {
Text("-", style = MaterialTheme.typography.headlineLarge, color = Color.Red)

}

TextButton(onClick = { viewModel.increaseQuantity(product) }) {
Text("+", style = MaterialTheme.typography.headlineLarge)
}

Button(onClick = { viewModel.removeFromCart(product) }) {
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = Color.Cyan)
}
}

}

// Right Side: Price
Text(
text = "${"%.2f".format(product.price * product.quantity)}" , // Fix here
//"${product.price * product.quantity}",
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp)
)
}
}

Search product




Shop View Model

Product List Each Product has:

  • id: Unique identifier.
  • name: The breed name.
  • price: Cost of the cat breed.
  • imageRes: Resource ID of the image.
  • description: Brief description.
  • diet: What type of food this breed prefers.
  • benefits: Special features of the breed.
  • quantity: Default is 1, but changes when added to cart.
Functions:
  • searchQuery is a mutable state to store the user's search input.
  • private set ensures only ViewModel can modify it.
  • If searchQuery is blank, it returns all products.
  • Otherwise, it filters products whose name contains the search query.
  • This function updates the search query when the user types in the search bar.
  • _cart is a mutable list that holds cart items.
  • cart is an immutable getter, so UI can read it but not modify directly.
  • Checks if the product already exists in the cart.
  • If not in cart, adds it with default quantity = 1.
  • Removes the product by matching its id.
  • Finds product index in _cart.
  • If product is found (index != -1), it creates a new copy of the product with quantity increased by 1.
  • If quantity > 1, decreases it.
  • If quantity = 1, removes the product from the cart.
  • Uses sumOf to calculate total cost of all products in cart.
  • Multiplies price by quantity for each product.
package com.codingbihar.flipkat

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class ShopViewModel : ViewModel() {

// Product List (Example Cat Breeds)
val productList = listOf(
Product(1, "Persian", 1499.99, R.drawable.cat1,
"Elegant and affectionate, ideal for calm homes.\n",
"Diet: High-protein dry/wet food, omega-rich for coat care.\n",
"Benefits: Perfect for apartment living, affectionate companion."
),
Product(2, "Maine Coon", 1899.99, R.drawable.cat2,
"Large, intelligent, and great for families.\n",
"Diet: Protein-rich meals, lean meats, and fish.\n",
"Benefits: Social, good with kids, excellent hunters."
),
Product(3, "Siamese", 1199.99, R.drawable.cat3,
"Playful, vocal, and highly social.\n",
"Diet: High-protein diet with lean meats, avoid high carbs.\n",
"Benefits: Intelligent, easy to train, very interactive."
),
Product(4, "Bengal", 1599.99, R.drawable.cat4,
"Energetic, intelligent, and loves climbing.\n" ,
"Diet: Raw meat, high-protein cat food, and fish.\n",
"Benefits: Active, loves playing, unique leopard-like coat."
),
Product(5, "Ragdoll", 1299.99, R.drawable.cat5,
"Gentle, affectionate, and perfect lap cats.\n",
"Diet: Wet/dry balanced food, prefers protein and healthy fats.\n",
"Benefits: Loving nature, great for families and seniors."
),
Product(6, "British Shorthair", 999.99, R.drawable.cat6,
"Calm, low-maintenance, and independent.\n",
"Diet: High-protein food, avoids excessive fats.\n",
"Benefits: Easy to care for, ideal for busy owners."
),
Product(7, "Scottish Fold", 1399.99, R.drawable.cat7,
"Sweet-tempered with unique folded ears.\n",
"Diet: Balanced dry/wet food, occasional fish treats.\n",
"Benefits: Adaptable, affectionate, good with other pets."
),
Product(8, "Sphynx", 1799.99, R.drawable.cat8,
"Hairless, warm, and loves human attention.\n",
"Diet: High-calorie meals, rich in proteins for body warmth.\n",
"Benefits: Hypoallergenic, loves cuddling, friendly personality."
),
Product(9, "Abyssinian", 1199.99, R.drawable.cat9,
"Highly active, intelligent, and loves to explore.\n",
"Diet: Meat-based diet, high protein, occasional chicken liver.\n",
"Benefits: Playful, loves climbing, great for interactive owners."
),
Product(10, "Birman", 1099.99, R.drawable.cat10,
"Friendly, affectionate, and easy to groom.\n",
"Diet: Dry and wet food mix, prefers protein-rich meals.\n",
"Benefits: Great with kids and other pets, social and loving."
)
)
var searchQuery by mutableStateOf("")
private set
private val _cart = mutableStateListOf<Product>()
val cart: List<Product> get() = _cart

// Filtered List Based on Search Query
val filteredProducts: List<Product>
get() = if (searchQuery.isBlank()) productList
else productList.filter { it.name.contains(searchQuery, ignoreCase = true) }

fun updateSearchQuery(query: String) {
searchQuery = query
}

fun addToCart(product: Product) {
val existingItem = _cart.find { it.id == product.id }
if (existingItem == null) {
_cart.add(product)
}
}

fun removeFromCart(product: Product) {
_cart.removeIf { it.id == product.id }
}

fun increaseQuantity(product: Product) {
val index = _cart.indexOfFirst { it.id == product.id }
if (index != -1) _cart[index] = _cart[index].copy(quantity = _cart[index].quantity + 1)
}

fun decreaseQuantity(product: Product) {
val index = _cart.indexOfFirst { it.id == product.id }
if (index != -1 && _cart[index].quantity > 1) {
_cart[index] = _cart[index].copy(quantity = _cart[index].quantity - 1)
} else {
_cart.removeAt(index)
}
}

fun getTotalCost(): Double {
return _cart.sumOf { it.price * it.quantity }
}
}


data class Product(
val id: Int,
val name: String,
val price: Double,
val imageRes: Int,
val description: String,
val diet: String, // New field for diet details
val benefits: String, // New field for benefits
var quantity: Int = 1 // Default quantity is 1
)

Payment Screen

  • Displays a list of cart items.
  • Shows the total cost of all items.
  • Provides a "Pay Now" button.
  • Calls viewModel.getTotalCost() to calculate total cost of the cart.
  • The result is stored in totalPrice.
  • Uses Column to arrange UI elements vertically.
  • systemBarsPadding() ensures safe spacing (avoiding overlaps with status bar).
  • Displays "Payment Screen" as a title.
  • Uses LazyColumn (optimized list) to display each product in the cart.
  • Calls PaymentItem(product) for each cart item.
  • Shows the total price of all products in the cart.
  • Right-aligned (align(Alignment.End)) for a better visual structure.
  • Adds spacing (Spacer) for proper UI structure.
  • "Pay Now" button is set to full width (fillMaxWidth()).
  • The button currently does nothing (onClick = { }), but you can integrate payment logic here.
  • The PaymentItem composable is responsible for displaying a single product inside the cart.
  • Wraps each cart item inside a Column.
  • Uses border() to create a gray-bordered card-like effect.
  • Uses Row to arrange image and details horizontally.
  • Displays the product image (painterResource(id = product.imageRes)).
  • Adds spacing (Spacer) between the image and text.
  • Uses another Column inside Row to align text vertically.

Displays:

  • Product name (headlineMedium).
  • Price per item (titleMedium).
  • Quantity of the item.
  • Calculates total price per item (price × quantity).
  • Displays it using headlineMedium.
package com.codingbihar.flipkat

import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp

@Composable
fun PaymentScreen(viewModel: ShopViewModel) {
// Total Sum of All Items
val totalPrice = viewModel.getTotalCost()
Column(modifier = Modifier.systemBarsPadding(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally) {
Text("Payment Screen", style = MaterialTheme.typography.headlineLarge)
LazyColumn(Modifier.fillMaxWidth().weight(1f)) {
items(viewModel.cart) { product ->
PaymentItem(product)
}
}
Text(
"Total: $totalPrice",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(end = 16.dp).align(Alignment.End)
)
Spacer(modifier = Modifier.height(16.dp))
// Pay Now Button
Button(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text("Pay Now")
}
Spacer(modifier = Modifier.height(16.dp))


}
}
@Composable
fun PaymentItem(product: Product) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.padding(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Product Image
Image(
painter = painterResource(id = product.imageRes),
contentDescription = "Product Image",
modifier = Modifier.size(80.dp)
)

Spacer(modifier = Modifier.width(8.dp))

// Product Details
Column(modifier = Modifier.weight(1f)) {
Text(product.name, style = MaterialTheme.typography.headlineMedium)
Text("${product.price} per item", style = MaterialTheme.typography.titleMedium)
Text("Quantity: ${product.quantity}", style = MaterialTheme.typography.titleMedium)
}

// Total Price for This Item
Text(
"${product.price * product.quantity}",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(end = 8.dp)
)
}
}
}

Previous Post Next Post

Contact Form