General Solution for Caching Specific Items in Scrollable Widgets
In this section, we’ll introduce a general solution for caching specific items within scrollable widgets.
First, let’s recall that when we discussed ListView, there was an addAutomaticKeepAlives
property that we didn’t cover. If addAutomaticKeepAlives
is set to true
, ListView will automatically wrap each list item with an AutomaticKeepAlive parent widget. While PageView and PageView.builder don’t expose this parameter directly, they ultimately generate a SliverChildDelegate responsible for loading list items on demand. In SliverChildDelegate, each time a list item is built, it gets wrapped with an AutomaticKeepAlive parent widget. Let’s first introduce the AutomaticKeepAlive widget.
44.1 AutomaticKeepAlive
The main function of the AutomaticKeepAlive widget is to mark the keepAlive
property of the root RenderObject of list items as true
or false
based on necessity. For simplicity, we can think of the root RenderObject as the root widget of a list item, representing the entire list item component. We will refer to the Viewport area of the list widget, plus the cacheExtent
(pre-render area), as the loading area:
When
keepAlive
is set tofalse
, if a list item scrolls out of the loading area, the list component will be destroyed.When
keepAlive
is set totrue
, after a list item scrolls out of the loading area, the Viewport caches the list component. When the list item re-enters the loading area, the Viewport will first check if it is cached, and if it is, the component is reused; otherwise, it is rebuilt.
So, when does AutomaticKeepAlive mark the keepAlive
property of a list item as true
or false
? The answer is: it’s up to the developer! Flutter implements a client-server mechanism, where AutomaticKeepAlive acts as the server, and its child components can act as clients. If a child component wants to change its cache status, it sends a notification (called a KeepAliveNotification) to AutomaticKeepAlive, which then adjusts the keepAlive
state and performs any necessary resource cleanup (such as releasing the cache when keepAlive
changes from true
to false
).
Building on the PageView example from the previous section, the idea for implementing page caching is simple: make the Page
a client of AutomaticKeepAlive. To make this easier for developers, Flutter provides the AutomaticKeepAliveClientMixin. All we need to do is mix this into the PageState
and add a few necessary operations:
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); // This must be called return Center(child: Text("${widget.text}", textScaleFactor: 5)); } @override bool get wantKeepAlive => true; // Indicates whether to cache }
The code is simple. We just need to provide a wantKeepAlive
value, which determines whether AutomaticKeepAlive should cache the current list item. Additionally, we must call super.build(context)
in the build
method. This is implemented in AutomaticKeepAliveClientMixin and sends a message to AutomaticKeepAlive based on the current wantKeepAlive
value. AutomaticKeepAlive will then perform its operations, as shown in Figure .
Now, if we rerun the example, each page will only be built once, meaning the caching has succeeded.
Note that if we use PageView.custom to build pages and don’t wrap list items with an AutomaticKeepAlive parent widget, the above solution won’t work. In this case, when the client sends a message, it won’t find the server, resulting in a "404 error." 😊
44.2 KeepAliveWrapper
Although we can use AutomaticKeepAliveClientMixin to quickly implement page caching, doing so by mixing it in is not always elegant. It requires modifying the Page
code, which can be intrusive and inflexible. For example, if a Page
component needs to be used both inside and outside of a list, and we want to cache it in the list, we would have to maintain two separate implementations. To address this, I’ve created a KeepAliveWrapper component. If a list item needs to be cached, simply wrap it with KeepAliveWrapper.
@override Widget build(BuildContext context) { var children = <Widget>[]; for (int i = 0; i < 6; ++i) { // Simply wrap the item with KeepAliveWrapper children.add(KeepAliveWrapper(child: Page(text: '$i'))); } return PageView(children: children); }
Here’s the implementation of KeepAliveWrapper:
class KeepAliveWrapper extends StatefulWidget { const KeepAliveWrapper({ Key? key, this.keepAlive = true, required this.child, }) : super(key: key); final bool keepAlive; final Widget child; @override _KeepAliveWrapperState createState() => _KeepAliveWrapperState(); } class _KeepAliveWrapperState extends State<KeepAliveWrapper> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); return widget.child; } @override void didUpdateWidget(covariant KeepAliveWrapper oldWidget) { if (oldWidget.keepAlive != widget.keepAlive) { // Update the keepAlive state updateKeepAlive(); } super.didUpdateWidget(oldWidget); } @override bool get wantKeepAlive => widget.keepAlive; }
Now, let’s test this with ListView:
class KeepAliveTest extends StatelessWidget { const KeepAliveTest({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListView.builder(itemBuilder: (_, index) { return KeepAliveWrapper( // If set to true, all list items will be cached and won’t be destroyed. // If set to false, items will be destroyed after leaving the preloaded area. // Be cautious when using this, as caching all items will increase memory consumption. keepAlive: true, child: ListItem(index: index), ); }); } } class ListItem extends StatefulWidget { const ListItem({Key? key, required this.index}) : super(key: key); final int index; @override _ListItemState createState() => _ListItemState(); } class _ListItemState extends State<ListItem> { @override Widget build(BuildContext context) { return ListTile(title: Text('${widget.index}')); } @override void dispose() { print('dispose ${widget.index}'); super.dispose(); } }
Since all the list items are cached, you won’t see any logs when scrolling the list, as shown in Figure .
As expected, no logs appear in the console. If we set keepAlive
to false
, once the list item moves out of the pre-rendered area, it will be destroyed, and logs will appear in the console, as shown in Figure .
As we can see, the KeepAliveWrapper works as expected. I have added this KeepAliveWrapper to the flukit widget library, so if you need it, you can find it there.