Building a Product Detail Page Widget with Related Items in Flutter
A Product Detail Page (PDP) is a critical component in any e-commerce or product-showcasing application. It provides users with comprehensive information about a specific product, facilitating their decision-making process. A well-designed PDP not only displays essential product details but also intelligently suggests related items, enhancing user engagement and potentially increasing conversion rates. This article will guide you through creating a robust Flutter widget for a Product Detail Page, complete with related product recommendations.
Understanding the Core Components
Before diving into the code, let's break down the essential elements of a typical Product Detail Page:
- Product Header: Features the product image, name, and price.
- Product Description: Detailed text explaining the product's features, benefits, and specifications.
- Call to Action (CTA): Buttons like "Add to Cart" or "Buy Now" that prompt users to take action.
- Related Products/Recommendations: A curated list of similar or complementary products, often displayed in a horizontally scrollable list.
Data Model
First, we need a data model to represent our product. This model will hold all the necessary information for a product, including details for related items.
class Product {
final String id;
final String name;
final String imageUrl;
final double price;
final String description;
final List<String> relatedProductIds; // List of IDs of related products
Product({
required this.id,
required this.name,
required this.imageUrl,
required this.price,
required this.description,
this.relatedProductIds = const [],
});
// Example factory for dummy data
factory Product.dummy(String id) {
return Product(
id: id,
name: 'Product $id',
imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=Product+$id',
price: 29.99 + int.parse(id) * 0.5,
description: 'This is a detailed description for Product $id. It highlights all the amazing features and benefits of this fantastic item.',
relatedProductIds: ['2', '3', '4'].where((e) => e != id).toList(), // Avoid self-relation
);
}
}
// Add copyWith to Product class for easier dummy data setup (for example usage)
extension ProductCopyWith on Product {
Product copyWith({
String? id,
String? name,
String? imageUrl,
double? price,
String? description,
List<String>? relatedProductIds,
}) {
return Product(
id: id ?? this.id,
name: name ?? this.name,
imageUrl: imageUrl ?? this.imageUrl,
price: price ?? this.price,
description: description ?? this.description,
relatedProductIds: relatedProductIds ?? this.relatedProductIds,
);
}
}
The Product Detail Page Widget Structure
Our `ProductDetailPage` will be a `StatelessWidget`. It will use a `Scaffold` for basic page structure and a `SingleChildScrollView` to ensure the content is scrollable, especially on smaller screens or when the description is long. Inside the scroll view, a `Column` will arrange the various sections vertically.
import 'package:flutter/material.dart';
// Assume Product class is defined as above
class ProductDetailPage extends StatelessWidget {
final Product product;
final List<Product> allProducts; // Needed to find related products by ID
const ProductDetailPage({
Key? key,
required this.product,
required this.allProducts,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Image
Image.network(
product.imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Name
Text(
product.name,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8.0),
// Product Price
Text(
'\$${product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.green[700],
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16.0),
// Product Description
Text(
product.description,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 24.0),
// Add to Cart Button
Center(
child: ElevatedButton(
onPressed: () {
// Implement add to cart logic
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart!')),
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: const Text(
'Add to Cart',
style: TextStyle(fontSize: 18),
),
),
),
const SizedBox(height: 32.0),
// Related Items Section
_buildRelatedItemsSection(context),
],
),
),
],
),
),
);
}
Widget _buildRelatedItemsSection(BuildContext context) {
final relatedProducts = allProducts
.where((p) => product.relatedProductIds.contains(p.id))
.toList();
if (relatedProducts.isEmpty) {
return const SizedBox.shrink(); // Hide section if no related products
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Related Products',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16.0),
SizedBox(
height: 200, // Fixed height for horizontal list of related items
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: relatedProducts.length,
itemBuilder: (context, index) {
final relatedProduct = relatedProducts[index];
return _buildRelatedProductCard(context, relatedProduct);
},
),
),
],
);
}
Widget _buildRelatedProductCard(BuildContext context, Product relatedProduct) {
return GestureDetector(
onTap: () {
// Navigate to the related product's detail page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
product: relatedProduct,
allProducts: allProducts, // Pass all products for subsequent related items
),
),
);
},
child: Container(
width: 150,
margin: const EdgeInsets.only(right: 16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8.0)),
child: Image.network(
relatedProduct.imageUrl,
height: 100,
width: double.infinity,
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.green[600],
),
),
],
),
),
],
),
),
);
}
}
Putting It All Together (Example Usage)
To see the `ProductDetailPage` in action, you can integrate it into a simple Flutter application. Below is an example of a `main.dart` file that creates a list of dummy products and navigates to the detail page when a product is tapped.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Dummy product data
final List<Product> dummyProducts = [
Product.dummy('1').copyWith(relatedProductIds: ['2', '3']),
Product.dummy('2').copyWith(relatedProductIds: ['1', '4']),
Product.dummy('3').copyWith(relatedProductIds: ['1', '2']),
Product.dummy('4').copyWith(relatedProductIds: ['2', '3']),
];
return MaterialApp(
title: 'Product App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ProductListPage(products: dummyProducts),
);
}
}
// A simple list page to navigate from
class ProductListPage extends StatelessWidget {
final List<Product> products;
const ProductListPage({Key? key, required this.products}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
leading: Image.network(product.imageUrl, width: 50, height: 50, fit: BoxFit.cover),
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
product: product,
allProducts: products, // Pass all products for related items
),
),
);
},
),
);
},
),
);
}
}
Key Considerations and Best Practices
- State Management: For more complex applications, consider using state management solutions like Provider, BLoC, or Riverpod to manage the product data and UI state effectively, especially for "Add to Cart" functionality or dynamic updates.
- Data Fetching: In a real-world scenario, product data and related items would be fetched asynchronously from an API or database. Implement loading indicators, error handling, and refresh mechanisms.
- User Experience (UX): Enhance the UX with smooth transitions, hero animations for images, and skeleton loaders while data is being fetched.
- Performance: Optimize image loading with caching solutions (e.g., `cached_network_image`). Ensure `ListView.builder` is used for long lists to efficiently render only visible items.
- Responsiveness: Adapt the layout for different screen sizes (phones, tablets) using `MediaQuery` or responsive layout widgets.
- Personalization: Related items can be further personalized based on user history, browsing behavior, or purchase patterns for a more tailored experience.
Conclusion
Building a Product Detail Page with related items in Flutter is straightforward once you understand its core components and how to leverage Flutter's powerful widget tree. By following the structure and code examples provided, you can create a functional and aesthetically pleasing PDP that enhances user engagement and supports your application's e-commerce goals. Remember to continuously refine the UI/UX and integrate robust data handling for a production-ready solution.