Swiping with PageView

  1. Objectives
  2. Code along
  3. Boilerplate
  4. Introduction to the PageView
    1. Mental Model of the PageView
  5. Implementing a simple PageView
    1. The Controller concept
    2. PageView children and builders
  6. Adding an indicator with TabPageSelector
  7. Other Resources

Objectives

Code along

https://youtu.be/gszjDeethgk

Boilerplate

This lab picks up where the Basic Navigation lab left off.

If you need all the code from that lab, or just want a clean starting point:

  1. Download the updated lists_and_grids_with_nav.zip and expand the zip file.
  2. Open the folder in VS Code.
  3. Click on the “Get Packages” popup, or open the Command Palette and run “Flutter: Get Packages”.

Introduction to the PageView

We showed two ways for displaying lists of information: the ListView and the GridView. Both of those widgets use a List (like an array) as a data source, build widgets to display individual items, and display the individual item widgets in a scrollable container.

A third option for displaying a list of items is a “gallery” or “page” style using the PageView widget. This is when you have individual items shown on a screen and can swipe left or right to travel through the list, like so:


Mental Model of the PageView

A clear mental model of how a PageView is assembled will help understand the code. Refer to the illustration below:

PageView component diagram

Implementing a simple PageView

We will implement a gallery-esque screen to swipe through our list of Bob Ross paintings using a PageView.

Our main.dart is getting cluttered, so we will put the PageView in a separate file for organizational purposes.

  1. Create a new file in the lib/ folder named lib/pageview-screen.dart.
  2. Paste in the following code:

     import 'package:flutter/material.dart';
     import 'main.dart';
     import 'bob_data.dart';
    
     class PageViewScreen extends StatelessWidget {
       PageViewScreen({super.key});
    
       @override
       Widget build(BuildContext context) {
         final List<BobRossPainting> paintings = BobRossData().paintings;
         final PageController _pageController = PageController();
    
         return Scaffold(
             appBar: AppBar(title: Text("PageView demo")),
             body: PageView(
               controller: _pageController,
               children: [
                 MyCard(painting: paintings[0]),
                 MyCard(painting: paintings[1]),
                 MyCard(painting: paintings[2]),
               ],
             ),
         );
       }
     }
    
  3. At the top of main.dart, add the line import 'package:lists_and_grids/pageview-screen.dart';
  4. In main.dart, add a new ElevatedButton that navigates to the new PageViewScreen.
  5. Run the app and navigate to the PageView.

Simple PageView with fixed list

The cards take up the entire screen, and there are only three paintings to show. We have not yet implemented the TabPageSelector to show the dots. But, try swiping back and forth and you will see some visual indicators from the PageView when you reach the ends of the list.

The Controller concept

In code, the PageView looks similar to a ListView in that it has children. The other notable attribute is the controller.

Controllers are a recurring concept in Flutter and are a part of many widgets. A controller is a class that tells a complicated Stateful widget (like PageView) how to behave. The intention of a controller is to enable the caller (you, the programmer using it) to manipulate the StatefulWidget more easily. Several of Flutter’s built-in widgets require controllers.

In this example, we need to instantiate the built-in PageController class and pass it to the PageView. The PageController exposes methods that let you change which page is shown programmatically, e.g., in response to a button press.

All PageViews require a PageController to work.

PageView children and builders

You can see in our current code that the children of the PageView are instances of the MyCard class, which is imported from main.dart at the top of the file. We could make the children be whatever we want. They could by MyCards, Texts, Images, Containers, etc. The children can be heterogenous, i.e., not all the same type.

Right now, the children are hardcoded to be MyCards based on the first three elements of the _data.paintings list. This is fine, but not scalable.

Builders are another important Flutter concept that we first saw in the the ListView and the GridView lab. Think of builders as a factory for creating widgets. A builder is a function that gets called when a widget needs something, like when a user scrolls or swipes through a widget. The widget says, “I need the 4th thing! Builder function, give me the widget for the 4th thing”, and the builder function returns the desired widget.

The _data.paintings variable has many elements. Rather than hardcoding each MyCard() child of PageView, let’s specify a builder that creates a MyCard for each element in the list.

Replace the PageView() in the body of your Scaffold with the following:

PageView.builder(
    controller: _pageController,
    itemCount: _data.paintings.length,
    itemBuilder: (context, index) =>
        MyCard(painting: _data.paintings[index]),
),

Your app should look the same as before, but now you will be able to swipe between all items in the _data.paintings list and not just the three hardcoded items we specified originally. Builders are a powerful concept and one that appears in many Flutter widgets.

Adding an indicator with TabPageSelector

The PageView works fine, but we currently do not indicate to the user where in the list of items they are. We could do this using text, such as “Item 3 / 24”, or using the familiar “dot indicator” that you have no doubt seen in mobile apps. In any case, we are going to need our screen to transform into a Stateful widget to keep track of where the user is in the list.

Despite the fact that the “dot indicator” is a common visual paradigm, adding it requires quite a few changes to our simple screen:

  1. In main.dart, change your navigation route from PageViewScreen() to PageViewScreenWithIndicator()
  2. In pageview-screen.dart, add the following:
class PageViewScreenWithIndicator extends StatefulWidget {
  const PageViewScreenWithIndicator({super.key});

  @override
  State<PageViewScreenWithIndicator> createState() =>
      _PageViewScreenWithIndicatorState();
}

class _PageViewScreenWithIndicatorState
    extends State<PageViewScreenWithIndicator>
    with SingleTickerProviderStateMixin {
  final List<BobRossPainting> paintings = BobRossData().paintings;

  final PageController _pageController = PageController();
  TabController? _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: paintings.length, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("PageView demo")),
      body: Column(
        children: [
          SizedBox(
            height: 400,
            child: PageView.builder(
              controller: _pageController,
              itemCount: paintings.length,
              onPageChanged: (value) => setState(() {
                _tabController?.index = value;
              }),
              itemBuilder: (context, index) =>
                  MyCard(painting: paintings[index]),
            ),
          ),
          TabPageSelector(
            controller: _tabController,
          ),
        ],
      ),
    );
  }
}

There is a lot to unpack here:

PageView with TabSelector

Wow, that seems like a lot, and it is. Some complicated things are simple with Flutter, and some seemingly-simple things are complicated. The real culprit here is managing state. Remember, for any Stateful widget, you need three things:

  1. A variable that maintains some state: in this case, it is our _tabController.
  2. A user interaction that updates the state: in this case, it is the user swipes that fire the onPageChanged property of the PageView
  3. A widget in our build() method that changes based on the state: in this case, the TabPageSelector.

If you can wrap your head around this flow of events, and how the widgets in this example play off one another, you are well on your way to being able to build just about any sort of complicated UI in Flutter.

Other Resources