Building a Product Detail Page Widget with Related Items Carousel and Review Section in Flutter
A Product Detail Page (PDP) is a critical component of any e-commerce application, serving as the central hub where users learn about a product before making a purchase decision. A well-designed PDP not only displays essential product information but also enhances user engagement through features like related item recommendations and customer reviews. This article will guide you through building a professional and functional Product Detail Page widget in Flutter, incorporating these key elements.
Core Components of a Product Detail Page
A robust PDP typically includes:
- Product Images: High-quality visuals, often in a gallery or carousel.
- Product Information: Name, price, description, and available options (e.g., size, color).
- Call-to-Action: "Add to Cart" or "Buy Now" button.
- Related Items Carousel: Suggestions for similar or complementary products.
- Review Section: User ratings and written reviews, crucial for social proof.
Flutter Project Setup and Product Model
Assuming you have a basic Flutter project set up, we'll start by defining a simple data model for our Product and Review objects. This will help structure the data we display on the PDP.
Product Model (product.dart)
class Product {
final String id;
final String name;
final String imageUrl;
final double price;
final String description;
final double averageRating;
final int totalReviews;
final List<Review> reviews;
final List<String> relatedProductIds; // For simplicity, just IDs
Product({
required this.id,
required this.name,
required this.imageUrl,
required this.price,
required this.description,
required this.averageRating,
required this.totalReviews,
required this.reviews,
required this.relatedProductIds,
});
}
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,
});
}
// Mock data for demonstration
final List<Product> mockProducts = [
Product(
id: 'p1',
name: 'Stylish Running Shoes',
imageUrl: 'https://via.placeholder.com/600x400/FF5733/FFFFFF?text=Running+Shoes',
price: 79.99,
description: 'Lightweight and comfortable running shoes perfect for your daily jog. Features breathable mesh and responsive cushioning.',
averageRating: 4.5,
totalReviews: 120,
reviews: [
Review(reviewerName: 'Alice Johnson', rating: 5.0, comment: 'Absolutely love these shoes! Super comfortable.', date: DateTime(2023, 10, 26)),
Review(reviewerName: 'Bob Williams', rating: 4.0, comment: 'Good value for money, slightly tight fit.', date: DateTime(2023, 10, 25)),
],
relatedProductIds: ['p2', 'p3'],
),
Product(
id: 'p2',
name: 'Wireless Bluetooth Earbuds',
imageUrl: 'https://via.placeholder.com/600x400/33FF57/FFFFFF?text=Earbuds',
price: 49.99,
description: 'Immersive sound experience with long-lasting battery life. Perfect for workouts and daily commutes.',
averageRating: 4.8,
totalReviews: 85,
reviews: [
Review(reviewerName: 'Charlie Green', rating: 5.0, comment: 'Amazing sound quality and battery!', date: DateTime(2023, 10, 27)),
],
relatedProductIds: ['p1', 'p4'],
),
Product(
id: 'p3',
name: 'Smart Fitness Tracker',
imageUrl: 'https://via.placeholder.com/600x400/3357FF/FFFFFF?text=Fitness+Tracker',
price: 129.99,
description: 'Monitor your health and fitness with advanced sensors. Track steps, heart rate, sleep, and more.',
averageRating: 4.2,
totalReviews: 60,
reviews: [
Review(reviewerName: 'Diana Prince', rating: 4.0, comment: 'Does everything I need, great app.', date: DateTime(2023, 10, 28)),
],
relatedProductIds: ['p1', 'p2'],
),
Product(
id: 'p4',
name: 'Portable Power Bank',
imageUrl: 'https://via.placeholder.com/600x400/FFFF33/000000?text=Power+Bank',
price: 29.99,
description: 'Keep your devices charged on the go with this high-capacity power bank. Sleek design and fast charging.',
averageRating: 4.6,
totalReviews: 95,
reviews: [
Review(reviewerName: 'Eve Adams', rating: 5.0, comment: 'Essential for travel, charges quickly.', date: DateTime(2023, 10, 29)),
],
relatedProductIds: ['p2', 'p3'],
),
];
Product? getProductById(String id) {
try {
return mockProducts.firstWhere((product) => product.id == id);
} catch (e) {
return null;
}
}
Product Detail Page Widget Structure
We'll create a ProductDetailPage widget that takes a productId as an argument. This page will use a SingleChildScrollView to ensure all content is scrollable, especially on smaller screens.
import 'package:flutter/material.dart';
import 'product.dart'; // Import the product model
class ProductDetailPage extends StatelessWidget {
final String productId;
const ProductDetailPage({Key? key, required this.productId}) : super(key: key);
@override
Widget build(BuildContext context) {
final Product? product = getProductById(productId);
if (product == null) {
return Scaffold(
appBar: AppBar(title: const Text('Product Not Found')),
body: const Center(
child: Text('The requested product could not be found.'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(product.name),
actions: [
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
// Share product functionality
},
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Product Image
Image.network(
product.imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: 300,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 2. Product Name & Price
Text(
product.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8.0),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.deepOrange),
),
const SizedBox(height: 16.0),
// 3. Product Description
Text(
product.description,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24.0),
// 4. Add to Cart Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
// Add to cart logic
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart!')),
);
},
icon: const Icon(Icons.shopping_cart),
label: const Text('Add to Cart', style: TextStyle(fontSize: 18)),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12.0),
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
),
),
),
const SizedBox(height: 32.0),
// 5. Related Items Carousel
_buildRelatedItemsSection(context, product.relatedProductIds),
const SizedBox(height: 32.0),
// 6. Review Section
_buildReviewSection(context, product),
],
),
),
],
),
),
);
}
Widget _buildRelatedItemsSection(BuildContext context, List<String> relatedProductIds) {
if (relatedProductIds.isEmpty) {
return const SizedBox.shrink();
}
final List<Product> relatedProducts = relatedProductIds
.map((id) => getProductById(id))
.whereType<Product>() // Filter out nulls
.toList();
if (relatedProducts.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'You might also like',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16.0),
SizedBox(
height: 200, // Height for the horizontal carousel
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: relatedProducts.length,
itemBuilder: (context, index) {
final relatedProduct = relatedProducts[index];
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: GestureDetector(
onTap: () {
// Navigate to related product detail page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(productId: relatedProduct.id),
),
);
},
child: Card(
elevation: 4.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
child: SizedBox(
width: 150,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12.0)),
child: Image.network(
relatedProduct.imageUrl,
height: 100,
width: 150,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
relatedProduct.name,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4.0),
Text(
'\$${relatedProduct.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, color: Colors.deepOrange),
),
],
),
),
],
),
),
),
),
);
},
),
),
],
);
}
Widget _buildReviewSection(BuildContext context, Product product) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Customer Reviews',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16.0),
Row(
children: [
Icon(Icons.star, color: Colors.amber[700], size: 30),
const SizedBox(width: 8.0),
Text(
'${product.averageRating.toStringAsFixed(1)} out of 5',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(width: 8.0),
Text(
'(${product.totalReviews} reviews)',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]),
),
],
),
const SizedBox(height: 24.0),
if (product.reviews.isEmpty)
const Text('No reviews yet. Be the first to review this product!')
else
ListView.builder(
shrinkWrap: true, // Important for nested ListViews
physics: const NeverScrollableScrollPhysics(), // Important for nested ListViews
itemCount: product.reviews.length,
itemBuilder: (context, index) {
final review = product.reviews[index];
return Card(
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
review.reviewerName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
const Spacer(),
Row(
children: List.generate(5, (starIndex) {
return Icon(
starIndex < review.rating ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 18,
);
}),
),
],
),
const SizedBox(height: 8.0),
Text(
review.comment,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Align(
alignment: Alignment.bottomRight,
child: Text(
'${review.date.day}/${review.date.month}/${review.date.year}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
),
),
],
),
),
);
},
),
],
);
}
}
Usage Example (main.dart)
To see the PDP in action, you can integrate it into your main.dart file.
import 'package:flutter/material.dart';
import 'product_detail_page.dart'; // Import your PDP widget
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Product App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Product Listing'),
),
body: ListView.builder(
itemCount: mockProducts.length,
itemBuilder: (context, index) {
final product = mockProducts[index];
return Card(
margin: const EdgeInsets.all(8.0),
elevation: 4.0,
child: ListTile(
leading: Image.network(
product.imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
),
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(productId: product.id),
),
);
},
),
);
},
),
);
}
}
Explanation of Key Sections:
Product Information Section:
The main product details like image, name, price, and description are laid out using a Column within a Padding widget for consistent spacing. An Image.network displays the product image, and Text widgets handle the textual information. The "Add to Cart" button is an ElevatedButton.icon styled for prominence.
Related Items Carousel:
The _buildRelatedItemsSection method uses a horizontal ListView.builder wrapped in a SizedBox to control its height. Each related product is displayed within a Card, which includes its image, name, and price. Tapping on a related item navigates to its own ProductDetailPage, demonstrating simple navigation.
Review Section:
The _buildReviewSection displays the overall average rating and total number of reviews. Individual reviews are rendered using another ListView.builder. It's crucial to set shrinkWrap: true and physics: const NeverScrollableScrollPhysics() for the nested ListView.builder of reviews to prevent scroll conflicts with the parent SingleChildScrollView.
Conclusion
By following these steps, you can create a feature-rich Product Detail Page in Flutter that not only showcases your products effectively but also enhances the user experience through engaging features like related item carousels and detailed customer reviews. This modular approach allows for easy maintenance and expansion, enabling you to add more functionalities like product variations, wishlists, or dynamic content loading from APIs in the future.