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 .
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.