Rakesh Shrivas
Rakesh Shrivas

Reputation: 671

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 screens, 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(),
          );
    },
  );
}

Upvotes: 22

Views: 28512

Answers (9)

dieppa
dieppa

Reputation: 190

Normally you should be aware of the onAttach method, but just create an extension to get the value when available:

extension PageControllerExtension on PageController {
  double getPageOrDefault({ double defaultValue = 0.0 }) {
    try {
      return page ?? defaultValue;
    } on Error catch (_, __) {}
    return 0.0;
  }
}

Upvotes: 1

Khaled
Khaled

Reputation: 2282

You can check if controller.page is initialized by using controller.hasClients

So maybe use controller.hasClients ? controller.page?.toInt() : 0

Upvotes: 1

Lexon Li
Lexon Li

Reputation: 145

Not exactly the solution to the person who asked the question, but the error happened for me and I found out I forgot to add my controller to my PageViewController (very rookie mistake):

PageView.builder(
    controller: _pageController,
    ...

Upvotes: 0

Jason Bennett
Jason Bennett

Reputation: 13

I was running into this error using a pair of Visibility() widgets with a button to switch between the two. For anyone in a similar position, all I had to do was set the 'maintainState' parameter to true.

Upvotes: 0

Dima Inc
Dima Inc

Reputation: 21

Hello you can simply type:

pageController.hasClients ? 
   pageController.page == 0 ? Text("Page 1") : Text("Page 2")
 : Text("Initialization")

Upvotes: 0

Michał Jarek
Michał Jarek

Reputation: 1

I don't know if this will help you, but it worked for me:

instead of:

int nextPage = _controller.page.round() + 1;

you can do:

late int nextPage = _controller.page.round() + 1;

Upvotes: 0

Reagan Realones
Reagan Realones

Reputation: 229

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);
    });

Upvotes: 7

Nuqo
Nuqo

Reputation: 4081

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();
      });
    });
  }

Upvotes: 24

Arthur Eudeline
Arthur Eudeline

Reputation: 655

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),
          ],
        ),
      ],
    );
  }
}

Upvotes: 18

Related Questions