17

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(),
            );
      },
    );
  }
Rakesh Shriwas
  • 611
  • 2
  • 7
  • 15

7 Answers7

21

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();
      });
    });
  }
Nuqo
  • 3,793
  • 1
  • 25
  • 37
17

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),
          ],
        ),
      ],
    );
  }
}
Arthur Eudeline
  • 575
  • 3
  • 18
8

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

Reagan Realones
  • 221
  • 3
  • 3
0

use this widget and modify it as you want:

class IndicatorsPageView extends StatefulWidget {
  const IndicatorsPageView({
    Key? key,
    required this.controller,
  }) : super(key: key);
  final PageController controller;

  @override
  State<IndicatorsPageView> createState() => _IndicatorsPageViewState();
}

class _IndicatorsPageViewState extends State<IndicatorsPageView> {
  int _currentPage = 0;
  @override
  void initState() {
    widget.controller.addListener(() {
      setState(() {
        _currentPage = widget.controller.page?.toInt() ?? 0;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    widget.controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(
        3,
        (index) => IndicatorPageview(isActive: _currentPage == index, index: index),
      ),
    );
  }
}

class IndicatorPageview extends StatelessWidget {
  const IndicatorPageview({
    Key? key,
    required this.isActive,
    required this.index,
  }) : super(key: key);
  final bool isActive;
  final int index;
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(right: 8),
      width: 16,
      height: 16,
      decoration: BoxDecoration(color: isActive ?Colors.red : Colors.grey, shape: BoxShape.circle),
    );
  }
}
0

I encountered a similar problem, but it's not even a problem, the point is that sometimes it is mandatory for the controller to have a listener. Are you sure there is a PageView() for your PageController?

0

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;
-1

Hello you can simply type:

pageController.hasClients ? 
   pageController.page == 0 ? Text("Page 1") : Text("Page 2")
 : Text("Initialization")
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 09 '23 at 05:01