I am currently making a calendar app that I would like to swipe right or left to go to the next or previous month. I am using a PageView by initially setting up an array with 3 items in it and the initial page being the second one. I would like to swipe right and add a page to the end. I would like to swipe left and add a page to the beginning. Currently, if you go to the right (adding pages to the end) it works great. But if you go to the left (adding pages to the beginning) there is some odd behavior and it doesn't work at all.

I have pasted a simple example below with counters. I am not sure if I am doing it right or if my logic is off. Can anyone let me know the correct way to do this?

import 'package:flutter/material.dart';

void main() => runApp(LimeApp());

class LimeApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pageview Test',
      home: MainPage(),
    );
  }
}

int _lowerCount = -1;
int _upperCount = 1;

class MainPage extends StatelessWidget {
  final List<Widget> _pages = <Widget>[
    new Center(child: new Text("-1", style: new TextStyle(fontSize: 60.0))),
    new Center(child: new Text("0", style: new TextStyle(fontSize: 60.0))),
    new Center(child: new Text("1", style: new TextStyle(fontSize: 60.0)))
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: EdgeInsets.symmetric(
          vertical: 50.0,
        ),
        child: PageView(
          onPageChanged: (pageId){
            if(pageId == _pages.length - 1){
              print("Last page, add page to end");
              _upperCount = _upperCount + 1;
              _pages.add(new Center(child: new Text(_upperCount.toString(), style: new TextStyle(fontSize: 60.0))));
            }
            if(pageId == 0){
              print("First page, add page to start");
              _lowerCount = _lowerCount - 1;
              _pages.insert(0, new Center(child: new Text(_lowerCount.toString(), style: new TextStyle(fontSize: 60.0))));
            }
          },
          controller: PageController(
            initialPage: 1,
          ),
          children: _pages,
        ),
      ),
    );
  }
}

Swiping Example:


Solution 1: dshukertjr

I got to this point, and I am kind of close, but not close enough...

import 'package:flutter/material.dart';

void main() => runApp(LimeApp());

class LimeApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pageview Test',
      home: MainPage(),
    );
  }
}

int _lowerCount = -1;
int _upperCount = 1;

class MainPage extends StatefulWidget {
  MainPage({
    Key key,
  }) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  final List<int> _pages = <int>[-1, 0, 1];

  final PageController pageController = PageController(initialPage: 1);

  Widget _buildPages(page) {
    return Center(
      child: Text(page.toString(), style: new TextStyle(fontSize: 60.0)),
    );
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    pageController.addListener(() {
      print("inside listener");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: EdgeInsets.symmetric(
          vertical: 50.0,
        ),
        child: PageView(
          onPageChanged: (pageId) {
            if (pageId == _pages.length - 1) {
              print("Last page, add page to end");
              _upperCount = _upperCount + 1;
              setState(() {
                _pages.add(_upperCount);
              });
            }
            if (pageId == 0) {
              print("First page, add page to start");
              _lowerCount = _lowerCount - 1;
              setState(() {
                _pages.insert(0, _lowerCount);
              });
              pageController.jumpToPage(1);
            }
            print(_pages);
          },
          controller: pageController,
          children: List<Widget>.generate(_pages.length, (index) {
            return _buildPages(_pages[index]);
          }),
        ),
      ),
    );
  }
}


Solution 2: chemamolins

I propose you the code below that works.

  • It must use a stateful widget to be able to use the setState() methods.
  • When inserting the first page you need to create a new list and assign it to the previous list. Testing just inserting them without creating a new list resulted in the pages being added at the end. You need to make the list non final to be able to change it.

But it has the following caveat.

  • When you change the page backwards the onPageChanged() keeps the page 0 so just after inserting it you go to the newly added page because it is page 0. And since you already are on page 0, you can only add new pages if you go forward and back again.

Hope it helps

import 'package:flutter/material.dart';

void main() => runApp(LimeApp());

class LimeApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pageview Test',
      home: MainPage(),
    );
  }
}

int _lowerCount = -1;
int _upperCount = 1;

class MainPage extends StatefulWidget {
  @override
  MainPageState createState() {
    return new MainPageState();
  }
}

class MainPageState extends State<MainPage> {
  List<Widget> _pages = <Widget>[
    new Center(child: new Text("-1", style: new TextStyle(fontSize: 60.0))),
    new Center(child: new Text("0", style: new TextStyle(fontSize: 60.0))),
    new Center(child: new Text("1", style: new TextStyle(fontSize: 60.0)))
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        margin: EdgeInsets.symmetric(
          vertical: 50.0,
        ),
        child: PageView(
          onPageChanged: (pageId) {
            if (pageId == _pages.length - 1) {
              print("Last page, add page to end");
              _upperCount = _upperCount + 1;
              _pages.add(new Center(child: new Text(_upperCount.toString(), style: new TextStyle(fontSize: 60.0))));
              setState(() {});
            }
            if (pageId == 0) {
              print("First page, add page to start");
              _lowerCount = _lowerCount - 1;
              Widget w = new Center(child: new Text(_lowerCount.toString(), style: new TextStyle(fontSize: 60.0)));
              _pages = [w]..addAll(_pages);
              setState(() {});
            }
          },
          controller: PageController(
            initialPage: 1,
          ),
          children: _pages,
        ),
      ),
    );
  }
}


Solution 3: Maciek

I had similar problem. I decided to use solution with SetState -> update (rebuild) list of pages (add new on beginning or end) and set current index to keep current page visible. Problem is that _onPageChanged method fired befor page is complettle changed (it means that method is fired DURNING animation). SetState causes jumping from not finished animation to displaying static page. I tested solution with animation listener (to find end of animation), but this method was fired in completely unpredictives moments. Finally I use _onPageChanged -> SetState with page list, but my list is extended to 15 element - I have smooth animation from today to +- 7 days. Next one not smooth and another 7 ok :) My users usually not move mor then 7 days from current day, but I'm not proud of this solution. Maybe someone has a better solution?

    import 'package:flutter/material.dart';
    import 'package:font_awesome_flutter/font_awesome_flutter.dart';
    import 'package:gymprogress/page-dashboard-widget.dart';
    import 'package:gymprogress/chooseDateFromCalendar.dart';
    import 'package:gymprogress/page-dashboard-timeline.dart';
    import 'package:gymprogress/globals.dart';
    import 'package:gymprogress/reusable/pageTemplate.dart';
    import 'package:random_color/random_color.dart';

    class Dashboard extends StatefulWidget {
      Dashboard({Key key}) : super(key: key);

      @override
      _DashboardState createState() => _DashboardState();
    }

    class _DashboardState extends State<Dashboard> {

      List<DashboardPageWidget> _daysList = [];
      List<DashboardPageWidget> daysList = [];

      DateTime _currentDate;
      int _currentPageIndex;
      int offset = 7;
      PageController _controller = PageController(initialPage: 7, keepPage: false );

      final List<ColorHue> _hueType = <ColorHue>[
        ColorHue.green,
        ColorHue.red,
        ColorHue.pink,
        ColorHue.purple,
        ColorHue.blue,
        ColorHue.yellow,
        ColorHue.orange
      ];
      ColorBrightness _colorLuminosity = ColorBrightness.veryLight;
      ColorSaturation _colorSaturation = ColorSaturation.highSaturation;

      @override
      void initState() {
        _currentDate = DateTime.now();
        super.initState();
        _goToDate(_currentDate);
      }

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

      @override
      Widget build(BuildContext context) {
        return Container(
          child: Stack(
            children: <Widget>[
              ScaffoldTemplate(body: body()),
            ],
          ),
        );
      }

      Widget body() {
        return Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              actionPanel(),
              Timeline(
                date: _currentDate,
                onPrevButton: () => _prevPressed(),
                onNextButton: () => _nextPressed(),

              ),
              Expanded(
                child: bodyPageController()
              ),
            ],
          ),
        );

      }

      Widget bodyPageController() {
        return PageView(
            scrollDirection: Axis.horizontal,
            controller: _controller,
            onPageChanged: (index) {_onPageChanged(index);
        },
        children: _daysList,
            );
      }

      _myRandomColor(){
        return RandomColor().randomColor(
                  colorHue: ColorHue.multiple(colorHues: _hueType),
                  colorSaturation: _colorSaturation,
                  colorBrightness: _colorLuminosity);
      }

      Widget actionPanel(){

        return Container(
          width: screenWidth,
          // height: 50,
          color: Color(0xFF068FFA),
          child: Padding(
            padding: const EdgeInsets.fromLTRB(0, 2, 0, 2),
            child: Row(
              children: <Widget>[

                Expanded(
                  flex: 1,
                  child: Container(
                    child: Column(
                      children: <Widget>[
                        IconButton(
                          tooltip: 'Dodaj swój nowy trening',
                          icon: FaIcon(FontAwesomeIcons.calendarPlus, color: Colors.white,),
                          color: Colors.red,
                          onPressed:  () => _goToDate(DateTime.now()),
                        ),
                      ],

                    ),
                  ),
                ),

                Expanded(
                  flex: 1,
                  child: Container(
                    child: Column(
                      children: <Widget>[
                        IconButton(
                          tooltip: 'Przejdź do dnia dzisiejszego',
                          icon: FaIcon(FontAwesomeIcons.calendarCheck, color: Colors.white,),
                          color: Colors.red,
                          onPressed:  () => _goToDate(DateTime.now()),
                        ),
                      ],

                    ),
                  ),
                ),

                Expanded(
                  flex: 1,
                  child: Container(
                    child: Column(
                      children: <Widget>[
                        IconButton(
                          tooltip: 'Wybierz datę z kalendarza',
                          icon: FaIcon(FontAwesomeIcons.calendarAlt, color: Colors.white,),
                          color: Colors.red,
                          onPressed: () => _openCalendar(),
                        ),
                      ],

                    ),
                  ),
                ),

              ],
            ),
          )
          );

      }

      _prevPressed(){
        setState(() {
          _controller.animateToPage(_currentPageIndex-1, duration: Duration(milliseconds: 200), curve: Curves.ease);
        });
      }

      _nextPressed(){
        setState(() {
          _controller.animateToPage(_currentPageIndex+1, duration: Duration(milliseconds: 200), curve: Curves.ease);
        });
      }

      _onPageChanged(index){
            _currentPageIndex = index;

            if((_currentPageIndex<1) || (_currentPageIndex>(2*offset-1))){
              _goToDate(_daysList[index].day.date);
            }
            setState(() {
              _currentDate = _daysList[_currentPageIndex].day.date;
            });
      }

      _goToDate(DateTime _date) async{

        await _buildPageView(_date);
        _controller.jumpToPage(offset);
      }

      _buildPageView(_date) {

        DateTime date = DateTime(_date.year, _date.month, _date.day);

        daysList = [];

        for (var i=0; i<(2*offset+1); i++){
          daysList.add(DashboardPageWidget(day: Day(date.add(Duration(days: i-offset)), _myRandomColor()),));
          print(i.toString());
        }

        setState(() {
          _daysList = daysList;
          _currentDate = date;
          _currentPageIndex = offset;
        });
      }

      _openCalendar() async{
        await Navigator.of(context).push(
        PageRouteBuilder(
          opaque: false, // set to false
          pageBuilder: (_, __, ___) => SelectDateFromCalendar(),
        )).then((value){
          print(value);
          _goToDate(value);
        });
      }

    }

    class Day{
      DateTime date;
      Color color;

      Day(DateTime date, Color color){
        this.date = date;
        this.color = color;
      }
    }


Solution 4: krjw

I think I got it working. I did not use the onPageChange event. Instead I listened to changes in the controller.


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  static const Widget _home = HomePage();
  @override
  Widget build(BuildContext context) => MaterialApp(
        home: _home,
      );
}

class HomePage extends StatefulWidget {
  const HomePage();

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late PageController _controller;

  // This points always to the mid-element in _list
  late int _initialIndex;

  // This should work with 3, 7, 11, ... odd elements. Mind the pattern!!!
  List<int> _list = [-2, -1, 0, 1, 2];

  @override
  void initState() {
    super.initState();
    // Calculate mid.
    _initialIndex = (_list.length / 2).floor();

    _controller = PageController(initialPage: _initialIndex, viewportFraction: 0.8);

    // This is where we listen to changes.
    _controller.addListener(() {
      // Get index according to the direction
      // _controller.page! > _initialIndex => swiping to the right, going to the left / previous element
      // _controller.page! < _initialIndex => swiping to the left, going to the right / next element
      final index = _controller.page! > _initialIndex ? _controller.page!.floor() : _controller.page!.ceil();

      if (index == _initialIndex) return;
      if (index == _initialIndex - 1) {
        _prev();
      } else if (index == _initialIndex + 1) {
        _next();
      }

    });
  }

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

  // Update list and jump to the middle element
  void _next() {
    setState(() {
      _list
        ..removeAt(0)
        ..insert(_list.length, _list.last + 1);
      // Update current DateTime here
    });
    _controller.jumpToPage(_initialIndex);
  }

  // Update list and jump to the middle element
  void _prev() {
    setState(() {
      _list
        ..insert(0, _list.first - 1)
        ..removeLast();
      // Update current DateTime here
    });
    _controller.jumpToPage(_initialIndex);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
          actions: <Widget>[
            Padding(
              padding: const EdgeInsets.only(right: 20.0),
              child: IconButton(
                icon: const Icon(Icons.arrow_back),
                onPressed: () {
                  _prev();
                },
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(right: 20.0),
              child: IconButton(
                icon: const Icon(Icons.arrow_forward),
                onPressed: () {
                  _next();
                },
              ),
            ),
          ],
        ),
    body: PageView.builder(
      controller: _controller,
      itemCount: _list.length,
      itemBuilder: (context, i) {
        // This is where you should put your widget that generates
        // the view of the month.
        // Calculate DateTime like so 'DateTime(initialDate.year, initialDate.month + _list[index], initialDate.day)'
        return Padding(
          padding: const EdgeInsets.all(32.0),
          child: Container(
            color: Colors.blueAccent[100],
            alignment: Alignment.center,
            child: Text(
              '${_list[i]}',
              style: const TextStyle(fontSize: 32),
            ),
          ),
        );
      }
    ),
  );
}