Flutter (43): PageView and page cache

Time: Column:Mobile & Frontend views:293

43.1 PageView

If you want to implement page transitions and tab layouts, you can use the PageView widget. It’s important to note that PageView is a very significant widget, especially in mobile development, as it’s commonly used. Many apps include tab switching effects, image carousels, or vertical swiping for video switching like in TikTok. All of these can be easily achieved with PageView.

PageView({
  Key? key,
  this.scrollDirection = Axis.horizontal, // Scroll direction
  this.reverse = false,
  PageController? controller,
  this.physics,
  List<Widget> children = const <Widget>[],
  this.onPageChanged,
  
  // Whether to snap to the next page when scrolling, or display based on scroll distance
  this.pageSnapping = true,
  // Used mainly for accessibility support, explained later
  this.allowImplicitScrolling = false,
  // Explained later
  this.padEnds = true,
})

Let’s look at an example of tab switching. To highlight the key points, each tab page will display just one number.

// Tab page
class Page extends StatefulWidget {
  const Page({
    Key? key,
    required this.text
  }) : super(key: key);

  final String text;

  @override
  _PageState createState() => _PageState();
}

class _PageState extends State<Page> {
  @override
  Widget build(BuildContext context) {
    print("build ${widget.text}");
    return Center(child: Text("${widget.text}", textScaleFactor: 5));
  }
}

Now, let’s create a PageView:

@override
Widget build(BuildContext context) {
  var children = <Widget>[];
  // Generate 6 tab pages
  for (int i = 0; i < 6; ++i) {
    children.add(Page(text: '$i'));
  }

  return PageView(
    // scrollDirection: Axis.vertical, // Scroll vertically
    children: children,
  );
}

When you run this, you can swipe left and right to switch between pages. The result is shown in Figure .

Flutter (43): PageView and page cache

If you set the scroll direction to vertical (as shown in the commented section above), the pages will switch with an up-down swipe.


43.2 Page Caching

When running the example above, you might notice that every time you switch pages, a new page is built. For example, if you swipe from the first page to the second and then back to the first, the console logs the following:

flutter: build 0
flutter: build 1
flutter: build 0

This indicates that PageView does not cache pages by default. Once a page moves off-screen, it is destroyed, unlike in ListView or GridView, where we can manually specify how far off-screen widgets should be pre-rendered and cached (using the cacheExtent parameter). Widgets are only destroyed once they move out of this pre-rendered area. Unfortunately, PageView does not have a cacheExtent parameter! However, caching pages is a common requirement in real-world scenarios. For example, in a news app with multiple channel pages, if there’s no caching, once you swipe to a new channel, the old one is destroyed, and when you swipe back, the data must be re-fetched, and the page must be rebuilt—very inefficient.

Since cacheExtent is a property of Viewport, and PageView builds a Viewport, why doesn’t it allow this parameter to be passed? With this question in mind, I examined the source code of PageView and found the following code:

child: Scrollable(
  ...
  viewportBuilder: (BuildContext context, ViewportOffset position) {
    return Viewport(
      // TODO: We should provide a way to set cacheExtent
      // independent of implicit scrolling:
      // https://github.com/flutter/flutter/issues/45632
      cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
      cacheExtentStyle: CacheExtentStyle.viewport,
      ...
    );
  },
)

We can see that although PageView doesn’t expose cacheExtent, it does set a pre-render area when allowImplicitScrolling is true. Note that in this case, the cache style is CacheExtentStyle.viewport, meaning cacheExtent indicates the length of the cache as a multiple of the viewport’s width. Setting cacheExtent to 1.0 means one page on either side is cached. With this in mind, if we set allowImplicitScrolling to true, we can cache one page ahead and one page behind. Let’s modify the code and run the example. On the first page, the console logs the following:

flutter: build 0
flutter: build 1 // Pre-renders the second page

When you swipe to the second page:

flutter: build 0
flutter: build 1
flutter: build 2 // Pre-renders the third page

When you swipe back to the first page, there’s no additional output, meaning the first page was cached and not rebuilt. However, if you swipe to the third page and then back to the first, you will see:

flutter: build 0

This is expected because when allowImplicitScrolling is set to true, only the pages directly before and after the current page are cached. So, when you swipe to the third page, the first page is destroyed.

Caching the adjacent pages is better than no caching, but it doesn’t completely solve the problem. Why doesn’t Flutter allow developers to specify a caching strategy when it seems like such a straightforward addition? To understand this, we need to explore what allowImplicitScrolling is. According to the documentation and the issue linked in the source code comments, setting cacheExtent in PageView can conflict with accessibility features on iOS. Therefore, there’s no good solution at the moment. Some developers might argue that their apps don’t need to consider accessibility, but for now, if you need full control over the cache extent, you can copy the PageView source code and pass the cacheExtent parameter yourself.

While copying the source code is simple, it’s not the most orthodox solution. Is there a more general approach? Yes! Remember we mentioned earlier that "scrollable widgets provide a general solution for caching child items." We will cover that in the next section.