Creating a Customisable Carousel in Flutter: A Step-by-Step Guide

Boris Dev
6 min readAug 8, 2023

--

Introduction:

In this article, we’ll walk through the process of building a simple carousel in Flutter — a popular UI pattern used to display a collection of items or pages in a horizontally scrollable manner. We’ll explore how to create a versatile and interactive carousel that includes page indicators and the ability to swipe between pages. Let’s get started!

Prerequisites:

Before diving into the implementation, make sure you have Flutter installed on your development environment. Additionally, a basic understanding of Flutter widgets and state management concepts will be helpful for following along.

Let’s make this carousel !!!

Step 1:

We’ll begin by creating the main screen of our application. This screen will host our carousel. In lib/main.dart, define the MyApp and MainScreen classes as follows. In build we will return Scaffold and Carousel widget which we will define in Step 2:

import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Carousel',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
late final CarouselController _carouselController;
bool _isLastPage = false;
@override
void initState() {
_carouselController = CarouselController();
super.initState();
}
@override
void dispose() {
_carouselController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const SizedBox(
height: 100,
),
Carousel(
pages: _onBoardigImages,
controller: _carouselController,
onPageChanged: ({required bool isLastPage}) => setState(() { _isLastPage = isLastPage; }),
),
],
),
);
}

Step 2:

Defining the Carousel Widget Let’s define the Carousel widget, which will serve as the container for our horizontally scrollable pages. The Carousel will be a StatefulWidget and will contain various properties and widgets to achieve our desired functionality. Here's the initial implementation:

import 'package:flutter/material.dart';

class Carousel extends StatefulWidget {
final List<Widget> pages;
final double height = 300;
final CarouselController? controller;
final Duration nextPageDuration = const Duration(milliseconds: 10000);
final void Function({required bool isLastPage})? onPageChanged;

const Carousel({required this.pages, this.controller, this.onPageChanged, super.key});

@override
State<Carousel> createState() => _CarouselState();
}

Step 3:

Implementing the Carousel Controller for managing the carousel’s state and controlling its behaviour, we’ll create a CarouselController class that extends ChangeNotifier. This controller will enable us to move to the next page and notify listeners when the carousel's state changes.

class CarouselController extends ChangeNotifier {
void toNextPage() {
// Add logic to move to the next page in the carousel
notifyListeners();
}
}

Step 4:

Implementing the Carousel State Next, let’s create the state class _CarouselState that extends State<Carousel>. In this class, we'll use a PageController to control the PageView widget, and we'll handle the carousel's UI updates based on user interactions.

class _CarouselState extends State<Carousel> {
final PageController _pageController = PageController(initialPage: 0);

@override
void initState() {
widget.controller?.addListener(_handleNextPage);
_pageController.addListener(_handlePageChanged);
super.initState();
}

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

@override
Widget build(BuildContext context) {
// Implement the UI representation of the carousel using PageView and indicators
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: widget.height,
child: PageView(
controller: _pageController,
children: widget.pages,
),
),
_CarouselRectangle(
rectangleNumber: widget.pages.length,
currentRectangle: _pageController.positions.isEmpty ? 0 : _pageController.page!.round(),
onRectangleTapped: _animateToPage,
)
],
);
}

// Add methods to handle carousel interactions, such as swiping and tapping indicators

}

Step 5:

Implementing the Page Indicators In a carousel, page indicators help users understand which page they are currently viewing. We’ll create a private stateless widget _CarouselRectangle responsible for rendering the indicators.

class _CarouselRectangle extends StatelessWidget {
final int rectangleNumber;
final int currentRectangle;
final double rectangleSize = 20;
final Function(int) onRectangleTapped;

const _CarouselRectangle({required this.rectangleNumber, required this.currentRectangle, required this.onRectangleTapped});

@override
Widget build(BuildContext context) {
// Implement the UI representation of the page indicators
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: Iterable.generate(rectangleNumber).map((i) {
// Create the individual rectangle indicator with GestureDetector for tapping
return Container(
padding: const EdgeInsets.all(5),
child: Container(
height: rectangleSize,
width: rectangleSize,
decoration: BoxDecoration(
color: (i == currentRectangle) ?
const Color.fromRGBO(17, 173, 200, 0.984) :
const Color.fromRGBO(2, 64, 75, 0.98),
shape: BoxShape.circle
),
child: GestureDetector(
onTap: () => onRectangleTapped(i),
),
),
);
}).toList(),
);
}
}

Step 6:

Handling Carousel Interactions The _CarouselState class should contain methods to handle carousel interactions, such as swiping between pages and tapping on the indicators.

class _CarouselState extends State<Carousel> {
// Previous code...

// Method to handle moving to the next page in the carousel
void _handleNextPage() {
_pageController.nextPage(duration: widget.nextPageDuration, curve: Curves.easeIn);
}

// Method to animate to a specific page when an indicator is tapped
void _animateToPage(int page) {
_pageController.animateToPage(page, duration: widget.nextPageDuration, curve: Curves.easeIn);
}

// Method to update the UI when the page changes
void _handlePageChanged() {
setState(() {});
if (widget.onPageChanged != null) {
widget.onPageChanged!(isLastPage: _pageController.page!.round() == widget.pages.length - 1);
}
}
}

Step 7:

Let’s create the images for our carousel. Of course you will have to make assets folder in which you will put as many images you want. Then you will have to define the _onBoardingImages list and the _images widget.

Widget _images(String image) {
return Image.asset(image);
}
// List of images which we will use in our carousel
final _onBoardigImages = [
_images('assets/images/tree-6792528_960_720.jpg'),
_images('assets/images/nature-3125912_1280.jpg'),
_images('assets/images/flowers-276014_640.jpg'),
_images('assets/images/tree-736885_640.jpg'),
];

Step 8:

Finally we made it! Let’s wrap whole code. My code is in one file for the purpose of this guide but you can split the code


import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Carousel',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:const MainScreen(),
);
}
}





class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
@override
State<MainScreen> createState() => _MainScreenState();
}


class _MainScreenState extends State<MainScreen> {
late final CarouselController _carouselController;
bool _isLastPage = false;
@override
void initState() {
_carouselController = CarouselController();
super.initState();
}
@override
void dispose() {
_carouselController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const SizedBox(
height: 100,
),
Carousel(
pages: _onBoardigImages,
controller: _carouselController,
onPageChanged: ({required bool isLastPage}) => setState(() { _isLastPage = isLastPage; }),
),
],
),
);
}
}



Widget _images(String image) {
return Image.asset(image);
}
final _onBoardigImages = [
_images('assets/images/tree-6792528_960_720.jpg'),
_images('assets/images/nature-3125912_1280.jpg'),
_images('assets/images/flowers-276014_640.jpg'),
_images('assets/images/tree-736885_640.jpg'),
];



class Carousel extends StatefulWidget {
final List<Widget> pages;
final double height = 300;
final CarouselController? controller;
final Duration nextPageDuration = const Duration(milliseconds: 10000);
final void Function({required bool isLastPage})? onPageChanged;
const Carousel({required this.pages, this.controller, this.onPageChanged, });
@override
State<Carousel> createState() => _CarouselState();
}


class CarouselController extends ChangeNotifier {
void toNextPage() {
// Add logic to move to the next page in the carousel
notifyListeners();
}
}


class _CarouselState extends State<Carousel> {
final PageController _pageController = PageController(initialPage: 0);
@override
void initState() {
widget.controller?.addListener(toNextPage);
_pageController.addListener(() {
setState(() {});
if (widget.onPageChanged != null) {
widget.onPageChanged!(isLastPage: _pageController.page!.round() == widget.pages.length - 1);
}
});
super.initState();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: widget.height,
child: PageView(
controller: _pageController,
children: widget.pages
),
),
_CarouselRectangle(
rectangleNumber: widget.pages.length,
currentRectangle: _pageController.positions.isEmpty ? 0 : _pageController.page!.round(),
onRectangleTapped: _animateToPage,
)
],
);
}
void toNextPage() {
_pageController.nextPage(duration: widget.nextPageDuration, curve: Curves.easeIn);
}
void _animateToPage(int page) {
_pageController.animateToPage(page, duration: widget.nextPageDuration, curve: Curves.easeIn);
}
}



class _CarouselRectangle extends StatelessWidget {
final int rectangleNumber;
final int currentRectangle;
final double rectangleSize = 20;
final Function(int) onRectangleTapped;
const _CarouselRectangle({required this.rectangleNumber, required this.currentRectangle, required this.onRectangleTapped});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: Iterable.generate(rectangleNumber).map((i) {
return Container(
padding: const EdgeInsets.all(5),
child: Container(
height: rectangleSize,
width: rectangleSize,
decoration: BoxDecoration(
color: (i == currentRectangle) ?
const Color.fromRGBO(17, 173, 200, 0.984) :
const Color.fromRGBO(2, 64, 75, 0.98),
shape: BoxShape.circle
),
child: GestureDetector(
onTap: () => onRectangleTapped(i),
),
),
);
}).toList(),
);
}
}

In this step-by-step guide, we’ve learned have to create a customisable carousel in Flutter. From implementing the carousel controller to creating the carousel widget, and managing interactions and animations, you’ve gained a solid foundation to build upon. You can use this carousel in your every project and modify it meet your requirements.

--

--