Building an Auto-Play Image Carousel Widget in Flutter

03 Jan 2026

9K

35K

Building an Auto-Play Image Carousel Widget in Flutter

Image carousels are a ubiquitous UI component, widely used to showcase multiple images or promotional content in a compact, engaging manner. In Flutter, building such a widget is straightforward, and adding an auto-play feature further enhances its dynamic appeal. This article will guide you through creating a reusable image carousel widget with automatic scrolling functionality using Flutter's powerful widgets.

Prerequisites

  • Basic understanding of Flutter and Dart.
  • Familiarity with StatefulWidget and its lifecycle methods.

Core Components

We'll leverage a few key Flutter widgets and Dart features to construct our carousel:

  • PageView: The fundamental widget for displaying scrollable pages. It's perfect for our image carousel as it handles swiping gestures naturally.
  • PageController: Used in conjunction with PageView to programmatically control which page is displayed, allowing us to implement auto-play.
  • Timer (from dart:async): Essential for creating the auto-play mechanism, enabling us to trigger page transitions at regular intervals.
  • StatefulWidget: Necessary to manage the current page index and the state of our auto-play timer.

Step-by-Step Implementation

1. Define Your Image Data

First, let's set up a simple list of image URLs that our carousel will display. For a real application, these might come from an API or local assets.


final List<String> imageUrls = [
  'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_1280.jpg',
  'https://cdn.pixabay.com/photo/2018/01/12/10/19/dandelion-3077366_1280.jpg',
  'https://cdn.pixabay.com/photo/2016/11/08/05/26/tree-1807519_1280.jpg',
  'https://cdn.pixabay.com/photo/2017/02/01/22/02/mountains-2031009_1280.jpg',
];

2. Create the AutoPlayImageCarousel Widget

We'll start by defining a StatefulWidget to manage the carousel's state, including the current page index and the auto-play timer.


import 'dart:async';
import 'package:flutter/material.dart';

class AutoPlayImageCarousel extends StatefulWidget {
  final List<String> imageUrls;
  final Duration autoPlayDuration;
  final Duration transitionDuration;
  final Curve transitionCurve;

  const AutoPlayImageCarousel({
    Key? key,
    required this.imageUrls,
    this.autoPlayDuration = const Duration(seconds: 3),
    this.transitionDuration = const Duration(milliseconds: 400),
    this.transitionCurve = Curves.easeIn,
  }) : super(key: key);

  @override
  State<AutoPlayImageCarousel> createState() => _AutoPlayImageCarouselState();
}

class _AutoPlayImageCarouselState extends State<AutoPlayImageCarousel> {
  late PageController _pageController;
  late Timer _timer;
  int _currentPage = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(initialPage: _currentPage);
    _startAutoPlay();
  }

  @override
  void dispose() {
    _timer.cancel();
    _pageController.dispose();
    super.dispose();
  }

  // ... (rest of the code will go here)
}

3. Implement Auto-Play Logic

The auto-play functionality is managed by a Timer. In _startAutoPlay, we set up a periodic timer that increments the _currentPage and tells the _pageController to animate to the next page. We also ensure the page index wraps around when it reaches the end of the list.


  void _startAutoPlay() {
    _timer = Timer.periodic(widget.autoPlayDuration, (timer) {
      if (_pageController.hasClients) {
        int nextPage = (_currentPage + 1) % widget.imageUrls.length;
        _pageController.animateToPage(
          nextPage,
          duration: widget.transitionDuration,
          curve: widget.transitionCurve,
        );
      }
    });
  }

4. Build the UI with PageView and Image Display

Inside the build method, we'll use a PageView.builder to efficiently display our images. We'll also wrap it in a Stack to potentially add page indicators later.


  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        PageView.builder(
          controller: _pageController,
          itemCount: widget.imageUrls.length,
          onPageChanged: (index) {
            setState(() {
              _currentPage = index;
            });
          },
          itemBuilder: (context, index) {
            return Image.network(
              widget.imageUrls[index],
              fit: BoxFit.cover,
              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 indicators will go here
      ],
    );
  }

5. Adding Page Indicators (Optional but Recommended)

To provide visual feedback on the current page, we can add a row of dots at the bottom of the carousel. We'll place these within the same Stack.


  Widget _buildPageIndicator() {
    return Positioned(
      bottom: 10.0,
      left: 0,
      right: 0,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: widget.imageUrls.map((url) {
          int index = widget.imageUrls.indexOf(url);
          return Container(
            width: 8.0,
            height: 8.0,
            margin: const EdgeInsets.symmetric(horizontal: 4.0),
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: _currentPage == index
                  ? Colors.white
                  : Colors.white.withOpacity(0.5),
            ),
          );
        }).toList(),
      ),
    );
  }

Then, include this indicator in the build method's Stack:


  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        PageView.builder(
          // ... (PageView builder code)
        ),
        _buildPageIndicator(), // Add the indicator here
      ],
    );
  }

Complete Code Example

Here's the full code for our AutoPlayImageCarousel widget, ready to be dropped into your Flutter project.


import 'dart:async';
import 'package:flutter/material.dart';

class AutoPlayImageCarousel extends StatefulWidget {
  final List<String> imageUrls;
  final Duration autoPlayDuration;
  final Duration transitionDuration;
  final Curve transitionCurve;
  final double height; // Added height property

  const AutoPlayImageCarousel({
    Key? key,
    required this.imageUrls,
    this.autoPlayDuration = const Duration(seconds: 3),
    this.transitionDuration = const Duration(milliseconds: 400),
    this.transitionCurve = Curves.easeIn,
    this.height = 200.0, // Default height
  }) : super(key: key);

  @override
  State<AutoPlayImageCarousel> createState() => _AutoPlayImageCarouselState();
}

class _AutoPlayImageCarouselState extends State<AutoPlayImageCarousel> {
  late PageController _pageController;
  late Timer _timer;
  int _currentPage = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController(initialPage: _currentPage);
    _startAutoPlay();
  }

  @override
  void dispose() {
    _timer.cancel();
    _pageController.dispose();
    super.dispose();
  }

  void _startAutoPlay() {
    _timer = Timer.periodic(widget.autoPlayDuration, (timer) {
      if (_pageController.hasClients) {
        int nextPage = (_currentPage + 1) % widget.imageUrls.length;
        _pageController.animateToPage(
          nextPage,
          duration: widget.transitionDuration,
          curve: widget.transitionCurve,
        );
      }
    });
  }

  Widget _buildPageIndicator() {
    return Positioned(
      bottom: 10.0,
      left: 0,
      right: 0,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: widget.imageUrls.map((url) {
          int index = widget.imageUrls.indexOf(url);
          return Container(
            width: 8.0,
            height: 8.0,
            margin: const EdgeInsets.symmetric(horizontal: 4.0),
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: _currentPage == index
                  ? Colors.white
                  : Colors.white.withOpacity(0.5),
            ),
          );
        }).toList(),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: widget.height,
      child: Stack(
        children: [
          PageView.builder(
            controller: _pageController,
            itemCount: widget.imageUrls.length,
            onPageChanged: (index) {
              setState(() {
                _currentPage = index;
              });
            },
            itemBuilder: (context, index) {
              return Image.network(
                widget.imageUrls[index],
                fit: BoxFit.cover,
                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),
                  );
                },
              );
            },
          ),
          if (widget.imageUrls.isNotEmpty) _buildPageIndicator(),
        ],
      ),
    );
  }
}

Usage Example:

To use this widget in your app, simply pass your list of image URLs:


class MyHomePage extends StatelessWidget {
  final List<String> myImageUrls = [
    'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_1280.jpg',
    'https://cdn.pixabay.com/photo/2018/01/12/10/19/dandelion-3077366_1280.jpg',
    'https://cdn.pixabay.com/photo/2016/11/08/05/26/tree-1807519_1280.jpg',
    'https://cdn.pixabay.com/photo/2017/02/01/22/02/mountains-2031009_1280.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Auto-Play Carousel')),
      body: Center(
        child: AutoPlayImageCarousel(
          imageUrls: myImageUrls,
          height: 250.0, // Custom height
          autoPlayDuration: const Duration(seconds: 4), // Custom auto-play speed
        ),
      ),
    );
  }
}

Conclusion

You've successfully built a dynamic, auto-playing image carousel widget in Flutter! By combining PageView for scrollable content, PageController for programmatic control, and Timer for automatic transitions, we've created a versatile component. This widget is customizable with parameters for auto-play duration, transition speed, and height, making it adaptable to various UI needs.

Further enhancements could include pausing the auto-play when a user manually swipes, adding custom navigation buttons, or integrating more sophisticated page indicators from a package. However, this foundation provides a robust starting point for any auto-playing image carousel in your Flutter applications.

Related Articles

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en