Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy
A well-designed Product Detail Page (PDP) is crucial for any e-commerce application. It's where potential customers gather information, compare options, and ultimately make purchasing decisions. In Flutter, building a dynamic and feature-rich PDP involves combining various UI components and managing state effectively. This article will guide you through creating a sophisticated PDP widget that includes essential elements like promo badges, a review carousel, related items, and a quick buy functionality.
Understanding the Core Components
Before diving into the code, let's break down the key features we aim to implement:
- Product Information: Displaying images, title, price, description, and quantity selector.
- Promo Badges: Visually highlighting special offers (e.g., "Sale," "New," "Limited Stock").
- Review Carousel: Showcasing customer feedback in an engaging, scrollable format.
- Related Items: Suggesting other products that might interest the user, increasing discoverability and sales.
- Quick Buy/Add to Cart: A prominent call-to-action button for immediate purchase or adding to the shopping cart.
Data Models
To manage product and review data, we'll start with simple data models. These can be extended with more fields as needed.
class Product {
final String id;
final String name;
final String description;
final double price;
final double? originalPrice; // For sale items
final List<String> imageUrls;
final double averageRating;
final int reviewCount;
final List<String> promoBadges; // e.g., "Sale", "New", "Limited Stock"
final List<Product> relatedProducts;
Product({
required this.id,
required this.name,
required this.description,
required this.price,
this.originalPrice,
required this.imageUrls,
required this.averageRating,
required this.reviewCount,
this.promoBadges = const [],
this.relatedProducts = const [],
});
}
class Review {
final String reviewerName;
final double rating;
final String comment;
final DateTime date;
Review({
required this.reviewerName,
required this.rating,
required this.comment,
required this.date,
});
}
Structuring the Product Detail Page Widget
A typical Flutter PDP will use a Scaffold with a SingleChildScrollView to ensure all content is scrollable. Inside, we'll use Column and Row widgets to arrange the various sections.
import 'package:flutter/material.dart';
// Assume Product and Review models are defined as above
class ProductDetailPage extends StatefulWidget {
final Product product;
final List<Review> reviews; // Can be fetched dynamically
const ProductDetailPage({
Key? key,
required this.product,
required this.reviews,
}) : super(key: key);
@override
_ProductDetailPageState createState() => _ProductDetailPageState();
}
class _ProductDetailPageState extends State<ProductDetailPage> {
int _currentImageIndex = 0;
int _selectedQuantity = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.product.name),
actions: [
IconButton(icon: Icon(Icons.share), onPressed: () {}),
IconButton(icon: Icon(Icons.favorite_border), onPressed: () {}),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProductImagesAndBadges(),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildProductTitleAndPrice(),
SizedBox(height: 10),
_buildRatingAndReviewSummary(),
SizedBox(height: 15),
_buildQuantitySelector(),
SizedBox(height: 15),
Text('Description', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 8),
Text(widget.product.description),
],
),
),
_buildReviewCarousel(),
_buildRelatedItemsSection(),
],
),
),
bottomNavigationBar: _buildQuickBuyButton(),
);
}
// Helper methods for building sections will go here
}
1. Product Images and Promo Badges
We'll use a Stack to overlay promo badges on top of the product image. A PageView.builder can be used for multiple product images, allowing users to swipe through them.
Widget _buildProductImagesAndBadges() {
return Container(
height: 300,
color: Colors.grey[200],
child: Stack(
children: [
PageView.builder(
itemCount: widget.product.imageUrls.length,
onPageChanged: (index) {
setState(() {
_currentImageIndex = index;
});
},
itemBuilder: (context, index) {
return Image.network(
widget.product.imageUrls[index],
fit: BoxFit.cover,
width: double.infinity,
);
},
),
if (widget.product.promoBadges.isNotEmpty)
Positioned(
top: 10,
left: 10,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.product.promoBadges.map((badge) {
Color badgeColor = Colors.red;
if (badge == "New") badgeColor = Colors.blue;
if (badge == "Limited Stock") badgeColor = Colors.orange;
return Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: badgeColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
badge,
style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}).toList(),
),
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.product.imageUrls.length, (index) {
return Container(
width: 8.0,
height: 8.0,
margin: EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentImageIndex == index ? Colors.blue : Colors.grey,
),
);
}),
),
),
],
),
);
}
Widget _buildProductTitleAndPrice() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.product.name,
style: Theme.of(context).textTheme.headline5!.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Row(
children: [
Text(
'\$${widget.product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headline6!.copyWith(color: Colors.green, fontWeight: FontWeight.bold),
),
if (widget.product.originalPrice != null) ...[
SizedBox(width: 8),
Text(
'\$${widget.product.originalPrice!.toStringAsFixed(2)}',
style: TextStyle(
decoration: TextDecoration.lineThrough,
color: Colors.grey,
fontSize: 16,
),
),
],
],
),
],
);
}
Widget _buildRatingAndReviewSummary() {
return Row(
children: [
Icon(Icons.star, color: Colors.amber, size: 20),
SizedBox(width: 4),
Text(
widget.product.averageRating.toStringAsFixed(1),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(width: 8),
Text(
'(${widget.product.reviewCount} Reviews)',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
Spacer(),
TextButton(
onPressed: () { /* Navigate to all reviews page */ },
child: Text('See All Reviews'),
),
],
);
}
Widget _buildQuantitySelector() {
return Row(
children: [
Text('Quantity:', style: Theme.of(context).textTheme.subtitle1),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5),
),
child: Row(
children: [
IconButton(
icon: Icon(Icons.remove, size: 20),
onPressed: _selectedQuantity > 1
? () {
setState(() {
_selectedQuantity--;
});
}
: null,
),
Text(
_selectedQuantity.toString(),
style: TextStyle(fontSize: 16),
),
IconButton(
icon: Icon(Icons.add, size: 20),
onPressed: () {
setState(() {
_selectedQuantity++;
});
},
),
],
),
),
],
);
}
2. Review Carousel
A horizontal ListView.builder or PageView.builder is perfect for displaying a carousel of reviews. We'll show a few key details for each review.
Widget _buildReviewCarousel() {
if (widget.reviews.isEmpty) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Text('No reviews yet. Be the first!', style: TextStyle(fontStyle: FontStyle.italic)),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text('Customer Reviews', style: Theme.of(context).textTheme.headline6),
),
Container(
height: 180, // Height for the review cards
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.reviews.length,
itemBuilder: (context, index) {
final review = widget.reviews[index];
return Container(
width: MediaQuery.of(context).size.width * 0.8, // Each card takes 80% of screen width
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: List.generate(5, (starIndex) {
return Icon(
starIndex < review.rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 18,
);
}),
),
SizedBox(height: 8),
Text(
review.comment,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14),
),
SizedBox(height: 8),
Text(
'- ${review.reviewerName}',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
Text(
review.date.toLocal().toString().split(' ')[0], // Format date
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
),
);
},
),
),
SizedBox(height: 16),
],
);
}
3. Related Items Section
Similar to the review carousel, a horizontal ListView.builder is excellent for displaying a row of related products. Each product can be a smaller card that leads to its own PDP.
Widget _buildRelatedItemsSection() {
if (widget.product.relatedProducts.isEmpty) {
return const SizedBox.shrink(); // Hide if no related products
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text('You Might Also Like', style: Theme.of(context).textTheme.headline6),
),
Container(
height: 200, // Height for the related product cards
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.product.relatedProducts.length,
itemBuilder: (context, index) {
final relatedProduct = widget.product.relatedProducts[index];
return GestureDetector(
onTap: () {
// Navigate to the related product's detail page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
product: relatedProduct,
reviews: [], // Fetch related product reviews
),
),
);
},
child: Container(
width: 150,
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Image.network(
relatedProduct.imageUrls.first,
fit: BoxFit.cover,
width: double.infinity,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
relatedProduct.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
SizedBox(height: 4),
Text(
'\$${relatedProduct.price.toStringAsFixed(2)}',
style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
),
],
),
),
],
),
),
),
);
},
),
),
SizedBox(height: 16),
],
);
}
4. Quick Buy / Add to Cart Button
The call-to-action button should be prominent and easily accessible, often fixed at the bottom of the screen using a BottomNavigationBar or bottomNavigationBar property of Scaffold.
Widget _buildQuickBuyButton() {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, -5),
),
],
),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: Icon(Icons.shopping_cart),
label: Text('Add to Cart', style: TextStyle(fontSize: 16)),
onPressed: () {
// Implement add to cart logic here
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_selectedQuantity}x ${widget.product.name} added to cart!')),
);
},
style: ElevatedButton.styleFrom(
primary: Colors.blue, // Button color
onPrimary: Colors.white, // Text color
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
SizedBox(width: 10),
Expanded(
child: ElevatedButton(
child: Text('Buy Now', style: TextStyle(fontSize: 16)),
onPressed: () {
// Implement quick buy logic here (e.g., navigate to checkout)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Proceeding to buy ${_selectedQuantity}x ${widget.product.name}')),
);
},
style: ElevatedButton.styleFrom(
primary: Colors.green, // Button color
onPrimary: Colors.white, // Text color
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
);
}
Putting It All Together (Example Usage)
To see this PDP in action, you can create some dummy data and push this widget onto the navigation stack.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Dummy Data for demonstration
final dummyReviews = [
Review(
reviewerName: 'Alice',
rating: 4.5,
comment: 'Great product, exactly as described!',
date: DateTime(2023, 10, 26)),
Review(
reviewerName: 'Bob',
rating: 5.0,
comment: 'Fast shipping and excellent quality. Highly recommended.',
date: DateTime(2023, 10, 20)),
Review(
reviewerName: 'Charlie',
rating: 3.0,
comment: 'It\'s okay, but I expected more for the price.',
date: DateTime(2023, 09, 15)),
];
final dummyRelatedProduct1 = Product(
id: 'rp1',
name: 'Smart Watch X',
description: 'The next generation smart watch with health tracking.',
price: 199.99,
imageUrls: ['https://via.placeholder.com/300/CCCCCC/FFFFFF?text=SmartWatch'],
averageRating: 4.2,
reviewCount: 75,
promoBadges: ['New'],
);
final dummyRelatedProduct2 = Product(
id: 'rp2',
name: 'Wireless Earbuds Pro',
description: 'Noise-cancelling earbuds for an immersive audio experience.',
price: 129.00,
imageUrls: ['https://via.placeholder.com/300/999999/FFFFFF?text=Earbuds'],
averageRating: 4.8,
reviewCount: 120,
promoBadges: ['Sale'],
);
final dummyProduct = Product(
id: 'p1',
name: 'Premium Bluetooth Speaker',
description:
'Experience high-fidelity audio with deep bass and crystal-clear highs. ' +
'Perfect for home or outdoor use with its long-lasting battery and durable design.',
price: 89.99,
originalPrice: 120.00,
imageUrls: [
'https://via.placeholder.com/400/FF0000/FFFFFF?text=Speaker+Front',
'https://via.placeholder.com/400/00FF00/FFFFFF?text=Speaker+Side',
'https://via.placeholder.com/400/0000FF/FFFFFF?text=Speaker+Back',
],
averageRating: 4.5,
reviewCount: 50,
promoBadges: ['Sale', 'Limited Stock'],
relatedProducts: [dummyRelatedProduct1, dummyRelatedProduct2],
);
return MaterialApp(
title: 'Flutter Product App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ProductDetailPage(
product: dummyProduct,
reviews: dummyReviews,
),
);
}
}
Conclusion
Building a comprehensive Product Detail Page in Flutter involves careful consideration of layout, data presentation, and user interaction. By leveraging widgets like Stack for badges, PageView.builder or ListView.builder for carousels, and managing state for quantities, you can create a highly engaging and effective PDP. Remember to adapt the styling and functionality to match your application's specific design language and business logic, especially for state management and backend integration.