DEV Community

Terry Yuen
Terry Yuen

Posted on • Edited on

Paging dot indicators with PageView

Flutter has a TabPageSelector widget which is used to show dots indicating how many pages are available to view and which page you are currently at.

▫ ▫ ▫ ▪ ▫

Unfortunately, it is only designed to hook into a TabBarView.

I wanted to use the dots indicator on a PageView widget, which are both similar looking widgets, but their controllers are incompatible.

💡 TabBarView is for creating a UI with tabs at the top. PageView is for creating a UI based on separate pages like a book.

The former is a subclass of ScrollController while the latter is a subclass of ChangeNotifier, so both are completely incompatible.

Bridge the two

If I wanted to bridge the two, PageView has a callback called onPageChanged(int) and TabController has a writable index property.

It's simply a case of:

PageView.builder(
  onPageChanged: (index) => _tabController.index = index,
  //...
)
Enter fullscreen mode Exit fullscreen mode

But TabController doesn't allow you to update the total length of pages after it has already been created. The only way to change it is to dispose it and then re-create it.

DefaultTabController

Another option is to use the DefaultTabController inherited widget which automatically handles creation and disposing.

But how do we get the handle to the DefaultTabController in the first place?

Because it is an inherited widget, we can use the DefaultTabController.of to reach down BuildContext to get the nearest instance, like this:

DefaultTabController.of(context)!.index = index;
Enter fullscreen mode Exit fullscreen mode

In total, the implementation looks like this:

//assuming this data
var pages = ["a", "b", "c"];

Widget build(BuildContext context) {
  return DefaultTabController(
    length: pages.length,
    child: Column(
      children: [
        PageView.builder(
          itemCount: pages.length,
          onPageChanged: (index) {
            DefaultTabController.of(context)!.index = index;
          }
          itemBuilder(_, index) {
            return Text(pages[index]);
          }
        ),
        TabPageSelector(),
      ],
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

When the user swipes to a different page, we get the DefaultTabController and update the index, which tells TabPageSelector to also update the dot indicators.

Holup

The astute readers among you will notice that climbing the BuildContext to the ancestor inherited widget wouldn't work when both are using the same BuildContext.

The below call would throw a null de-reference error at the bang.

DefaultTabController.of(context)!.index
Enter fullscreen mode Exit fullscreen mode

To make it work, we need to insert another Builder in between the two, which will force the creation of another BuildContext.

In total, the implementation looks like this:

Widget build(BuildContext context) {
  return DefaultTabController(
    length: pages.length,
    child: Builder(builder: (context) {
      return Column(
        children: [
          PageView.builder(
            itemCount: pages.length,
            onPageChanged: (index) {
              DefaultTabController.of(context)!.index = index;
            }
            itemBuilder(_, index) {
              return Text(pages[index]);
            }
          ),
          TabPageSelector(),
        ],
      );
    }),
  );
}
Enter fullscreen mode Exit fullscreen mode

Happy coding!

Top comments (0)