Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter : PageController.page cannot be accessed before a PageView is built with it

How to solve the exception -

Unhandled Exception: 'package:flutter/src/widgets/page_view.dart': Failed assertion: line 179 pos 7: 'positions.isNotEmpty': PageController.page cannot be accessed before a PageView is built with it.

Note:- I used it in two screens and when I switch between screen it shows the above exception.

@override
  void initState() {
    super.initState();
      WidgetsBinding.instance.addPostFrameCallback((_) => _animateSlider());
  }

  void _animateSlider() {
    Future.delayed(Duration(seconds: 2)).then(
      (_) {
        int nextPage = _controller.page.round() + 1;

        if (nextPage == widget.slide.length) {
          nextPage = 0;
        }

        _controller
            .animateToPage(nextPage,
                duration: Duration(milliseconds: 300), curve: Curves.linear)
            .then(
              (_) => _animateSlider(),
            );
      },
    );
  }
like image 950
Rakesh Shriwas Avatar asked Apr 06 '20 11:04

Rakesh Shriwas


3 Answers

I don't have enough information to see exactly where your problem is, but I just encountered a similar issue where I wanted to group a PageView and labels in the same widget and I wanted to mark active the current slide and the label so I was needing to access controler.page in order to do that. Here is my fix :

Fix for accessing page index before PageView widget is built using FutureBuilder widget

class Carousel extends StatelessWidget {
  final PageController controller;

  Carousel({this.controller});

  /// Used to trigger an event when the widget has been built
  Future<bool> initializeController() {
    Completer<bool> completer = new Completer<bool>();

    /// Callback called after widget has been fully built
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      completer.complete(true);
    });

    return completer.future;
  } // /initializeController()

  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        // **** FIX **** //
        FutureBuilder(
          future: initializeController(),
          builder: (BuildContext context, AsyncSnapshot<void> snap) {
            if (!snap.hasData) {
              // Just return a placeholder widget, here it's nothing but you have to return something to avoid errors
              return SizedBox();
            }

            // Then, if the PageView is built, we return the labels buttons
            return Column(
              children: <Widget>[
                CustomLabelButton(
                  child: Text('Label 1'),
                  isActive: controller.page.round() == 0,
                  onPressed: () {},
                ),
                CustomLabelButton(
                  child: Text('Label 2'),
                  isActive: controller.page.round() == 1,
                  onPressed: () {},
                ),
                CustomLabelButton(
                  child: Text('Label 3'),
                  isActive: controller.page.round() == 2,
                  onPressed: () {},
                ),
              ],
            );
          },
        ),
        // **** /FIX **** //
        PageView(
          physics: BouncingScrollPhysics(),
          controller: controller,
          children: <Widget>[
            CustomPage(),
            CustomPage(),
            CustomPage(),
          ],
        ),
      ],
    );
  }
}

Fix if you need the index directly in the PageView children

You can use a stateful widget instead :

class Carousel extends StatefulWidget {
  Carousel();

  @override
  _HomeHorizontalCarouselState createState() => _CarouselState();
}

class _CarouselState extends State<Carousel> {
  final PageController controller = PageController();
  int currentIndex = 0;

  @override
  void initState() {
    super.initState();

    /// Attach a listener which will update the state and refresh the page index
    controller.addListener(() {
      if (controller.page.round() != currentIndex) {
        setState(() {
          currentIndex = controller.page.round();
        });
      }
    });
  }

  @override
  void dispose() {
    controller.dispose();

    super.dispose();
  }

  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
           Column(
              children: <Widget>[
                CustomLabelButton(
                  child: Text('Label 1'),
                  isActive: currentIndex == 0,
                  onPressed: () {},
                ),
                CustomLabelButton(
                  child: Text('Label 2'),
                  isActive: currentIndex == 1,
                  onPressed: () {},
                ),
                CustomLabelButton(
                  child: Text('Label 3'),
                  isActive: currentIndex == 2,
                  onPressed: () {},
                ),
              ]
        ),
        PageView(
          physics: BouncingScrollPhysics(),
          controller: controller,
          children: <Widget>[
            CustomPage(isActive: currentIndex == 0),
            CustomPage(isActive: currentIndex == 1),
            CustomPage(isActive: currentIndex == 2),
          ],
        ),
      ],
    );
  }
}
like image 53
Arthur Eudeline Avatar answered Nov 14 '22 15:11

Arthur Eudeline


I think you can just use a Listener like this:

int _currentPage;

  @override
  void initState() {
    super.initState();
    _currentPage = 0;
    _controller.addListener(() {
      setState(() {
        _currentPage = _controller.page.toInt();
      });
    });
  }
like image 37
Nuqo Avatar answered Nov 14 '22 14:11

Nuqo


This means that you are trying to access PageController.page (It could be you or by a third party package like Page Indicator), however, at that time, Flutter hasn't yet rendered the PageView widget referencing the controller.

Best Solution: Use FutureBuilder with Future.value

Here we just wrap the code using the page property on the pageController into a future builder, such that it is rendered little after the PageView has been rendered.

We use Future.value(true) which will cause the Future to complete immediately but still wait enough for the next frame to complete successfully, so PageView will be already built before we reference it.

class Carousel extends StatelessWidget {

  final PageController controller;

  Carousel({this.controller});

  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[

        FutureBuilder(
          future: Future.value(true),
          builder: (BuildContext context, AsyncSnapshot<void> snap) {
            
            //If we do not have data as we wait for the future to complete,
            //show any widget, eg. empty Container
            if (!snap.hasData) {
             return Container();
            }

            //Otherwise the future completed, so we can now safely use the controller.page
            return Text(controller.controller.page.round().toString);
          },
        ),

        //This PageView will be built immediately before the widget above it, thanks to
        // the FutureBuilder used above, so whenever the widget above is rendered, it will
        //already use a controller with a built `PageView`        

        PageView(
          physics: BouncingScrollPhysics(),
          controller: controller,
          children: <Widget>[
           AnyWidgetOne(),
           AnyWidgetTwo()
          ],
        ),
      ],
    );
  }
}

Alternatively

Alternatively, you could still use a FutureBuilder with a future that completes in addPostFrameCallback in initState lifehook as it also will complete the future after the current frame is rendered, which will have the same effect as the above solution. But I would highly recommend the first solution as it is straight-forward

 WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
     //Future will be completed here 
     // e.g completer.complete(true);
    });

like image 7
Reagan Realones Avatar answered Nov 14 '22 13:11

Reagan Realones