Creating a custom PageView with Flutter

Loredana Zdrânc
Zipper Studios
Published in
5 min readAug 12, 2019

Hello, Flutter developer! How are you today? If your task is to build a custom bidirectional and infinite scrollable PageView with Flutter, you are in for a treat!

“Expressive and Flexible UI”, this is one of the advantages that Flutter developers said this new technology has. When I build my custom PageView with Flutter I understand why. It’s your time to understand how you can create the beautiful PageView in the video below using the Flutter platform.

Together, we will create the screen above step-by-step and will begin to understand what is the role of each widget added. First, we need a model for a sports interest object with two parameters: title and image assets.

class InterestsModel {
String imageAsset;
String title;
InterestsModel(this.title, this.imageAsset);
}

The next step is to generate the class responsible for building the screen and to add a list of three InterestsModel objects, as below.

class ChooseInterestsScreen extends StatefulWidget {
@override
_ChooseInterestsScreenState createState() =>
new _ChooseInterestsScreenState();
}
class _ChooseInterestsScreenState extends State<ChooseInterestsScreen> { List interests = [
InterestsModel('Snowboarding', 'assets/ic_snowboarding_gray.png'),
InterestsModel('BMX', 'assets/ic_bike_gray.png'),
InterestsModel('Skateboarding', 'assets/ic_skateboarding_gray.png')
];
}

For the root, we will add a Scaffold widget that implements the basic material design visual layout structure. In order to add the background color, we will use a Container whose child will be a Column used to display the widgets as a vertical array. The build method looks like:

@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Color.fromRGBO(36, 43, 47, 1),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildTitle(),
_buildInterestsContent(),
_buildCheckIcon(),
_buildNextButton()
],
)),
);
}

The _buildTitle() method creates the “Choose your interests” title.

Text _buildTitle() {
return Text(
"Choose your interests".toUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
color: Color.fromRGBO(243, 243, 243, 1),
fontSize: 19,
fontFamily: 'RadikalMedium'),
);
}

_buildInterestsContent() is responsible for creating the interests PageView. It also adds the left and right shadows that cover the first and the last items of the PageView.

Stack _buildInterestsContent() {
return Stack(
children: <Widget>[
_buildInterestsPageView(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Image.asset('assets/bg_gradient_left.png'),
Image.asset('assets/bg_gradient_right.png'),
],
),
],
);
}

Flutter has three types of PageView:

  • PageView — we use this type when we have a limited number of items.
  • PageView.builder — used to generate an infinite number of pages. It builds children on demand.
  • PageView.custom — used to generate custom functionality for how the children are built.

Because we have three items that repeat infinitely, we use the PageView.builder which takes an itemBuilder function with context and currentIndex as parameters. Inside this method, we will build the PageView items using the _buildPageViewItem(). This method generates the list with selected items from the initial interests list based on the current index of the PageView. But, the currentIndex increases infinitely and if we call _buildPageViewItem() with an index greater than 2, we will generate the RangeError(index):Invalid value: Not range 0..2. That’s why we will add the current index logic inside the itemBuilder() method.

List selectedInterests = []; 
PageController pageController =
PageController(viewportFraction: 0.45, initialPage: 4240);
Container _buildInterestsPageView() {
return Container(
height: 210,
child: PageView.builder(
itemBuilder: (context, int currentIdx) {
int crt = currentIdx;
if (crt > 2) {
if (crt % 3 == 0) {
crt = 0;
} else if ((crt + 1) % 3 == 0) {
crt = 2;
} else if ((crt - 1) % 3 == 0) {
crt = 1;
}
}
return Container(
margin: const EdgeInsets.only(top: 31.0),
child: _buildPageViewItem(interests[crt], crt),
);
},
controller: pageController,
),
);
}
GestureDetector _buildPageViewItem(InterestsModel data, int crt) {
bool active = selectedInterests.contains(crt);
final String backgroundAsset = active
? 'assets/bg_yellow_bordered.png'
: 'assets/bg_gray_bordered.png';
return GestureDetector(
onTap: () {
setState(() {
if (selectedInterests.contains(crt))
selectedInterests.remove(crt);
else {
selectedInterests.add(crt);
}
});
},
child: Column(
children: <Widget>[
Text(data.title.toUpperCase(),
style: TextStyle(
color: Color.fromRGBO(243, 243, 243, 1),
fontFamily: 'RadikalThin',
fontSize: 11.0,
)),
SizedBox(
height: 129,
width: 129,
child: Container(
margin: const EdgeInsets.only(top: 5),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(backgroundAsset))),
child: Image.asset(data.imageAsset),
)),
],
),
);
}

As you can see, the PageView.builder sets a controller: pageController. This is used to set a viewportFraction and an initialPage. If we do not set the viewportFraction we cannot see all the items simultaneously on the screen. If we set this parameter at 0.45, we will obtain the required result. We set the initialPage at a big value because we need a bidirectional PageView. If the initial page is not set we will not have items on the left. Run the code without setting this controller parameters to see the effect.

Left image, the viewportFraction property is not set. Right image, the initialPage attribute is not set.

Add the check icon using the _buildCheckIcon() method below. If there are no selected interests, the check icon is gray otherwise, it's yellow.

Container _buildCheckIcon() {
String buttonAsset = selectedInterests.isEmpty
? 'assets/ic_check_gray.png'
: 'assets/ic_check_yellow_rounded.png';
return Container(
child: Image.asset(buttonAsset),
);
}

All we have to do is add the NEXT button. We can do that using the method below.

Container _buildNextButton() {
return Container(
alignment: Alignment.center,
margin: const EdgeInsets.only(left: 71, right: 71, top: 21),
child: SizedBox(
width: double.infinity,
height: 45,
child: RaisedButton(
child: Text(
"Next".toUpperCase(),
style: TextStyle(
color: Color.fromRGBO(40, 48, 52, 1),
fontFamily: 'RadikalMedium',
fontSize: 19),
),
color: Colors.white,
elevation: 4.0,
onPressed: () {},
),
),
);
}

That’s all! Run your project and enjoy your beautiful PageView with Flutter.

The full code can be found on Github.

If this article was useful, do not forget to give some claps. The comments section below is for you, your questions and your suggestions. They are all welcomed.

Thanks for reading!

https://www.zipperstudios.co

Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.

--

--