Paging dot indicators with PageView
Tags: flutter, pageview, tabpageselector
Published: 2021-12-14 04:08:23

Flutter has a [`TabPageSelector`](https://api.flutter.dev/flutter/material/TabPageSelector-class.html) 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`](https://api.flutter.dev/flutter/material/TabBarView-class.html).

I wanted to use the dots indicator on a [`PageView`](https://api.flutter.dev/flutter/widgets/PageView-class.html) 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.

- `PageView` only accepts a [`PageController`](https://api.flutter.dev/flutter/widgets/PageController-class.html)
- `TabPageSelector` only accepts a [`TabController`](https://api.flutter.dev/flutter/material/TabController-class.html).

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:
```dart
PageView.builder(
  onPageChanged: (index) => _tabController.index = index,
  //...
)
```

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`](https://api.flutter.dev/flutter/material/DefaultTabController-class.html) 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:

```dart
DefaultTabController.of(context)!.index = index;
```

In total, the implementation looks like this:

```dart
//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(),
      ],
    ),
  );
}
```

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.

```dart
DefaultTabController.of(context)!.index
```

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:
```dart
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(),
        ],
      );
    }),
  );
}
```

Happy coding!