image

03 Feb 2026

9K

35K

Crafting a Product Details Widget with Image Zoom in Flutter

A compelling product details page is crucial for e-commerce applications, allowing users to thoroughly examine items before making a purchase. A key feature enhancing this experience is an interactive image display with zoom capabilities. This article will guide you through creating a professional product details widget in Flutter, incorporating an effective image zoom functionality.

Prerequisites

Basic understanding of Flutter development and Dart programming. Ensure you have Flutter installed and set up.

Core Components of Our Widget

Our product details widget will consist of several key sections:

  • Image Carousel/Display: To showcase multiple product images.
  • Image Zoom: Allowing users to pinch-to-zoom for detailed inspection.
  • Product Information: Displaying the product's name, price, description, and other relevant details.
  • Action Buttons: Such as "Add to Cart" or "Buy Now".

Step 1: Setting up the Product Model

First, let's define a simple data model for our product.


class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final List<String> imageUrls;

  Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.imageUrls,
  });
}

Step 2: Implementing the Image Display with Zoom

Flutter's InteractiveViewer widget is perfectly suited for implementing pinch-to-zoom functionality. We will combine it with a PageView to allow users to swipe through multiple product images and zoom into the currently displayed one.


import 'package:flutter/material.dart';

class ProductImageZoomer extends StatefulWidget {
  final List<String> imageUrls;

  const ProductImageZoomer({Key? key, required this.imageUrls}) : super(key: key);

  @override
  State<ProductImageZoomer> createState() => _ProductImageZoomerState();
}

class _ProductImageZoomerState extends State<ProductImageZoomer> {
  int _currentPage = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: PageView.builder(
            itemCount: widget.imageUrls.length,
            onPageChanged: (index) {
              setState(() {
                _currentPage = index;
              });
            },
            itemBuilder: (context, index) {
              return InteractiveViewer(
                // Max zoom level (e.g., 2.5x original size)
                maxScale: 2.5,
                // Min zoom level (e.g., 0.8x original size)
                minScale: 0.8,
                // Restricts panning to within the image bounds after zooming
                // boundless: false, // Uncomment and experiment if needed
                child: Image.network(
                  widget.imageUrls[index],
                  fit: BoxFit.contain, // Use contain to ensure full image is visible initially
                  loadingBuilder: (context, child, loadingProgress) {
                    if (loadingProgress == null) return child;
                    return Center(
                      child: CircularProgressIndicator(
                        value: loadingProgress.expectedTotalBytes != null
                            ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
                            : null,
                      ),
                    );
                  },
                  errorBuilder: (context, error, stackTrace) {
                    return const Center(child: Icon(Icons.broken_image, size: 50, color: Colors.grey));
                  },
                ),
              );
            },
          ),
        ),
        // Page indicator
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(widget.imageUrls.length, (index) {
            return Container(
              width: 8.0,
              height: 8.0,
              margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: (Theme.of(context).primaryColor)
                    .withOpacity(_currentPage == index ? 0.9 : 0.4),
              ),
            );
          }),
        ),
      ],
    );
  }
}

Step 3: Building the Product Details Widget

Now, let's integrate the image zoomer and add other product information to create the full product details page.


import 'package:flutter/material.dart';
// Assuming Product and ProductImageZoomer are defined in the same file or imported

class ProductDetailsWidget extends StatelessWidget {
  final Product product;

  const ProductDetailsWidget({Key? key, required this.product}) : 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 Section with Zoom
            SizedBox(
              height: 300, // Fixed height for the image section
              child: ProductImageZoomer(imageUrls: product.imageUrls),
            ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: const TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '\$${product.price.toStringAsFixed(2)}',
                    style: TextStyle(
                      fontSize: 22,
                      color: Theme.of(context).primaryColor,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Description:',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    product.description,
                    style: const TextStyle(fontSize: 16),
                  ),
                  const SizedBox(height: 24),
                  // Action Button
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton.icon(
                      onPressed: () {
                        // Implement 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),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Step 4: Integrating into Your Application

To see your widget in action, you can use it in your main.dart file. For better project structure, you might place Product, ProductImageZoomer, and ProductDetailsWidget in separate files (e.g., models/product.dart, widgets/product_image_zoomer.dart, screens/product_details_screen.dart).


import 'package:flutter/material.dart';
// Import your product model and widgets if placed in separate files
// import 'package:your_app/models/product.dart';
// import 'package:your_app/widgets/product_image_zoomer.dart';
// import 'package:your_app/screens/product_details_screen.dart';


void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Example Product Data
    final Product dummyProduct = Product(
      id: '1',
      name: 'Stylish Backpack',
      description: 'A durable and stylish backpack suitable for daily use, travel, and school. Features multiple compartments and water-resistant material, ensuring your belongings stay safe and organized.',
      price: 49.99,
      imageUrls: [
        'https://images.unsplash.com/photo-1553062407-98eeb64c6a6f?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        'https://images.unsplash.com/photo-1549429402-dd320579e09d?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        'https://images.unsplash.com/photo-1582106294528-662a420b7274?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
      ],
    );

    return MaterialApp(
      title: 'Product Details App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ProductDetailsWidget(product: dummyProduct),
    );
  }
}

Best Practices and Enhancements

  • Error Handling: Implement robust error handling for image loading (as shown with errorBuilder) to gracefully handle failed image requests.
  • Loading Indicators: Provide visual feedback during image loading (as shown with loadingBuilder) to improve user experience.
  • Caching: For better performance and reduced data usage, consider using an image caching library like cached_network_image, especially when dealing with many network images.
  • Responsiveness: Ensure your widget adapts well to different screen sizes and orientations. Using Flexible, Expanded, and MediaQuery can help create a responsive layout.
  • Accessibility: Add semantic labels for screen readers to enhance accessibility for all users.
  • State Management: For more complex applications, consider using a dedicated state management solution (e.g., Provider, Bloc, Riverpod) for managing cart state, product selection, and other dynamic data.
  • Zoom Reset: When navigating between images in PageView, the InteractiveViewer state (zoom level, pan position) will automatically reset for each new image, which is generally desired. If you need to persist or manually control the zoom, you would manage TransformationController.

Conclusion

Building an engaging product details page with interactive image zoom is essential for a modern e-commerce experience. By leveraging Flutter's powerful widgets like InteractiveViewer and PageView, developers can create highly functional and visually appealing interfaces with relatively little effort. This guide provides a solid foundation for implementing such a feature, which can be further enhanced and customized to fit specific application requirements, ensuring users have all the tools they need to examine products thoroughly.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

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