preface

Those of you who have developed a Flutter app will know that when using ListView or TabBarView, the status of a list item may be lost if you leave it unhandled. For example, if a list item is currently selected, slide it out and then back out, the selected status will disappear. The list item is restored to an unchecked state. To fix this problem, we need only in the State of the Widget list item with AutomaticKeepAliveClientMixin, and write wantKeepAlive returns true, Finally, call super.build(context) within build(). After this is done, the state of the list item remains no matter how slippery it is.

/ / with ` AutomaticKeepAliveClientMixin `
class _MyListItemState extends State<MyListItem> with AutomaticKeepAliveClientMixin{
  @override
  Widget build(BuildContext context) {
  / / call ` super. Build ` (context)
    super.build(context);
    return ListTile(
      title: Text('Item ${widget.index + 1}')); }@override
  // Overwriting 'wantKeepAlive' returns' true '
  bool get wantKeepAlive => true;
}
Copy the code

It doesn’t seem that complicated, but there’s a little bit of a question, why does this keep the state? What’s going on behind the scenes? Why override wantKeepAlive? I mixed with AutomaticKeepAliveClientMixin not telling you to hold state, why bother? And that call to super.build(context) looks redundant and easy to ignore and cause errors.

To answer these questions, we’ll have to dig deep into the source code to see how AutomaticKeepAlive works.

The source code

To make it easier to understand, “keep alive” is used directly in this article. Let’s look at the source code of AutomaticKeepAliveClientMixin.

AutomaticKeepAliveClientMixin

mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
  KeepAliveHandle? _keepAliveHandle;

  void_ensureKeepAlive() { _keepAliveHandle = KeepAliveHandle(); KeepAliveNotification(_keepAliveHandle!) .dispatch(context); }void_releaseKeepAlive() { _keepAliveHandle! .release(); _keepAliveHandle =null;
  }

  @protected
  bool get wantKeepAlive;

  
  @protected
  void updateKeepAlive() {
    if (wantKeepAlive) {
      if (_keepAliveHandle == null)
        _ensureKeepAlive();
    } else {
      if(_keepAliveHandle ! =null) _releaseKeepAlive(); }}@override
  void initState() {
    super.initState();
    if (wantKeepAlive)
      _ensureKeepAlive();
  }

  @override
  void deactivate() {
    if(_keepAliveHandle ! =null)
      _releaseKeepAlive();
    super.deactivate();
  }

  @mustCallSuper
  @override
  Widget build(BuildContext context) {
    if (wantKeepAlive && _keepAliveHandle == null)
      _ensureKeepAlive();
    return const_NullWidget(); }}Copy the code

The source code as a whole is not complicated, _ensureKeepAlive is called at initState and build when wantKeepAlive is true. _releaseKeepAlive is called when deactivate is activated. The function name indicates that the state needs to be saved, and that the state does not need to be saved. So let’s just focus on these two functions. From the _ensureKeepAlive function we get two classes KeepAliveNotification and KeepAliveHandle that we haven’t seen before. But we can see that both sets and cancels are doing the work. Set KeepAliveNotification call with a KeepAliveHandle. To cancel, just call _keepAliveHandle! .release(), doesn’t have anything to do with KeepAliveNotification.

It feels like a handle is registered at KeepAliveNotification. When you cancel it, you can cancel it through this handle.

class KeepAliveNotification extends Notification {
  
  final Listenable handle;
}
Copy the code

KeepAliveNotification is derived from Notification, so its job is obviously to send notifications to one of its ancestors, And you can assume that the ancestor node type is NotificationListener

.

class KeepAliveHandle extends ChangeNotifier {
  voidrelease() { notifyListeners(); }}Copy the code

KeepAliveHandle inherits from the familiar ChangeNotifier. Visible in AutomaticKeepAliveClientMixin we can see it used for two very common way of communication between nodes in a Flutter.

The graph looks something like this:

So the next problem is to find the recipient, which is NotificationListener

and see what’s going on.

AutomaticKeepAlive

So where do I find NotificationListener

? It’s right in front of you. AutomaticKeepAliveClientMixin same dart files.

  void _updateChild() {
    _child = NotificationListener<KeepAliveNotification>(
      onNotification: _addClient,
      child: widget.child!,
    );
  }
Copy the code

So in this function up here, _updateChild(), we can see NotificationListener

, and this function belongs to _AutomaticKeepAliveState, which is a State, The corresponding StatefulWidget is naturally AutomaticKeepAlive. The obvious thing to focus on is the implementation of _AutomaticKeepAliveState. Here we are just looking at some key source code. It is obvious from the function body above that the handler that receives NotificationListener

notification messages is _addClient. Let’s take a look at how this notification message is handled.

bool _addClient(KeepAliveNotification notification) {
    finalListenable handle = notification.handle; _handles ?? = <Listenable, VoidCallback>{}; _handles! [handle] = _createCallback(handle); handle.addListener(_handles! [handle]!) ;if(! _keepingAlive) { _keepingAlive =true;
      final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
      if(childElement ! =null) {
        _updateParentDataOfChild(childElement);
      } else{ SchedulerBinding.instance! .addPostFrameCallback((Duration timeStamp) {
          if(! mounted) {return;
          }
          final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
          assert(childElement ! =null);
          _updateParentDataOfChild(childElement!);
        });
      }
    }
    return false;
  }
Copy the code

The function body is quite long, but it does two things in summary. First, it wraps the handle as a VoidCallback, and then sets this callback as its own listener and stores it in a Map. Then go to the child node of type ParentDataElement

. The call _updateParentDataOfChild was found.

So we can see that setting the keepalive notification here will update the ParentData of a child node. What about the unlive call? Obviously you have to see what that callback does.

VoidCallback _createCallback(Listenable handle) {
    return() { _handles! .remove(handle);if(_handles! .isEmpty) {if(SchedulerBinding.instance! .schedulerPhase.index < SchedulerPhase.persistentCallbacks.index) { setState(() { _keepingAlive =false; });
        } else {       
          _keepingAlive = false;
          scheduleMicrotask(() {
            if(mounted && _handles! .isEmpty) { setState(() { }); }}); }}}; }Copy the code

Two things too, remove yourself from that Map, and if the Map is empty, it sets _keepingAlive to false, and triggers a rebuild.

Then the last thing to focus on is the build method.

@override
  Widget build(BuildContext context) {
    
    return KeepAlive(
      keepAlive: _keepingAlive,
      child: _child!,
    );
  }
Copy the code

The return is a KeepAlive.

KeepAlive

This Widget inherits from ParentDataWidget

. ParentData = ParentData = ParentData Let’s just focus on the applyParentData implementation:

void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is KeepAliveParentDataMixin);
    final KeepAliveParentDataMixin parentData = renderObject.parentData! as KeepAliveParentDataMixin;
    if(parentData.keepAlive ! = keepAlive) { parentData.keepAlive = keepAlive;final AbstractNode? targetParent = renderObject.parent;
      if (targetParent isRenderObject && ! keepAlive) targetParent.markNeedsLayout();// No need to redo layout if it became true.}}Copy the code

What it does is it finds the child RenderObject and sets keepAlive.

At this point, the whole process of setting up the live is complete. So what we’re going to do is we’re going to go all the way around and we’re going to end up with one of the renderObjects under the list being ParentData. It looks something like this:

The problem is that we set a flag bit for the RenderObject to keep the State alive. What’s the relationship between them? In order to understand this, Sliver has to be mentioned. Everything in Flutter that involves efficient rolling will almost always use a Sliver subframe. Include the ListView and TabBarView needed to keep this article alive. Sliver itself is quite complex, and there are several more articles to be written to give a general overview, but you can refer to other authors’ articles on Sliver if you want. Since this article is concerned with state preservation, it will only address Sliver caching as it relates to preservation.

RenderSliverMultiBoxAdaptor

Silver live processing is preserved in the two functions in the class RenderSliverMultiBoxAdaptor but this class name we know this is a subclass of RenderObject, so keep alive or not to keep alive to see it how to deal with their own children RenderObject. The two functions involved are _createOrObtainChild and _destroyOrCacheChild:

  void _destroyOrCacheChild(RenderBox child) {
    final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
    if(childParentData.keepAlive) { remove(child); _keepAliveBucket[childParentData.index!]  = child; child.parentData = childParentData;super.adoptChild(child);
      childParentData._keptAlive = true;
    } else{ _childManager.removeChild(child); }}Copy the code

Start with the _destroyOrCacheChild function, which is called when the list item needs to be recycled after it has been completely destroyed. At this point, the RenderObject is either destroyed completely or cached for future use. It is the keepAlive flag that determines whether to destroy or cache. When childParentData. KeepAlive to true, the child RenderObject was stored in the _keepAliveBucket array. Otherwise it is destroyed by calling _childManager.removechild. Now, you might be wondering, well, how can I keep State when I’m caching a RenderObject? The reason is that _childManager.removechild is not called in the cache branch. The _childManager is SliverMultiBoxAdaptorElement, see the name we know it’s RenderSliverMultiBoxAdaptor corresponding element. So _childManager. RemoveChild hadn’t called the child RenderObject corresponding Element is still living in SliverMultiBoxAdaptorElement, Element alive. The states in the child tree are also alive. The purpose of survival is achieved.

Once cached, there will be a time to retrieve it and reuse it, using the _createOrObtainChild call

void _createOrObtainChild(int index, { required RenderBox? after }) {
    invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
    
      if (_keepAliveBucket.containsKey(index)) {
        finalRenderBox child = _keepAliveBucket.remove(index)! ;final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
        
        dropChild(child);
        child.parentData = childParentData;
        insert(child, after: after);
        childParentData._keptAlive = false;
      } else{ _childManager.createChild(index, after: after); }}); }Copy the code

The logic is that if there is a RenderObject in the _keepAliveBucket array, use it. Otherwise, let _childManager create a new child node based on the current index. Here we create the Element and RenderObject subtrees of our list items.

rightAutomaticKeepAliveClientMixinSome thoughts on

Through the analysis of the source above, we on the mechanism of action of AutomaticKeepAliveClientMixin have a certain understanding. So, there are some details when using AutomaticKeepAliveClientMixin we considered will be needed.

  • Do I only need mindless Settings when I need to save statuswantKeepAlivefortrueAnd that’s it?

To discuss the problem when we are going to think of status, what is the cost of living, and the price is obviously outside the display area, keep alive the list item corresponds to the element tree and widget subtree has been retained in memory, which takes up more memory space, if there are tens of thousands of data in our list, If wantKeepAlive is set to true, they will all stay in memory. If I use sliver, I can use SingleChildScrollView. If I use sliver, I can use SingleChildScrollView. If I use Sliver, I can use SingleChildScrollView.

Therefore, the correct idea would be to set wantKeepAlive to true only when the state is really needed and to false when it is not, so that it can be recycled. The fewer list items you need to preserve state, the better.

For example, if I have a page with a list of cities to choose, and this is a single list, I can’t choose Shanghai if I choose Beijing, so if this list needs to keep the selection state, it’s better not to mindlessly set wantKeepAlive to true, otherwise hundreds of cities and counties may live in the memory. In this case, it is better to keep only the state of the selected city and default wantKeepAlive to false for the others. So there’s only one cached. So Flutter also close in updateKeepAlive AutomaticKeepAliveClientMixin provides us with a function, so that when you need to update keep alive state:

@protected
  void updateKeepAlive() {
    if (wantKeepAlive) {
      if (_keepAliveHandle == null)
        _ensureKeepAlive();
    } else {
      if(_keepAliveHandle ! =null) _releaseKeepAlive(); }}Copy the code

For this list example, we just need to call updateKeepAlive to update the keepalive state when the selection state changes. Also don’t leave wantKeepAlive as true, change it to something like the following:

@override
  // _selected indicates the selected state. True indicates the selected state; false indicates the unselected state
  bool get wantKeepAlive => _selected;
Copy the code

The above is a radio selection example, but what if my list is a multi-selection list? Like a mailing list, I might have to select them all and then delete them and so on?

Much choose this kind of situation we’re going to consider, if is limited in the alternative, such as at most 5 10 at a time, you can refer to the above examples of radio to handle, but if is do not limit, the possible ways of this dynamic Settings did not reduce the memory footprint, at this point we can consider to raise its status, Promote the selection state to the parent node of the list, or you can use a state-management tool such as Provider to do so. The list item itself not saved state, this case, we don’t need with AutomaticKeepAliveClientMixin, or further, is completely use StatelessWidget without StatefulWidget.

  • When you set upwantKeepAlivefortrue/falseDoes it have to be preserved/not preserved?

If you list items in only one State with the AutomaticKeepAliveClientMixin, then the State to determine whether the reservation status, but if your list item is a complex widgets, There are more than one State with the AutomaticKeepAliveClientMixin, then each State calls after _ensureKeepAlive in _handles left a record. Each call to _releaseKeepAlive removes the record from _HANDLES, but it is important to note that _keepingAlive is set to false only if _handles are empty. In other words, in the case of multiple states, the State will be reclaimed only if all states declare that the State is not retained. Therefore, the conclusion is that if wantKeepAlive is set to true, the State will be retained; if wantKeepAlive is set to false, the State will not be retained. It’s going to have to see if any other states are still asking for reservations.

Another important point to note is that in the case of nested slivers, setting the reserved state only applies to the sliver closest to you. Let’s say you nested a ListView inside a TabBarView. Then declare the state to be preserved in the list item, and that declaration will only apply to the ListView. If you cut to another TAB and then cut back, the state is lost.

conclusion

This article through the source code of AutomaticKeepAlive made a detailed introduction, I believe that after reading we have a certain understanding of the principle of state retention in Sliver. And then to us in the use of AutomaticKeepAliveClientMixin may encounter some problems when do the corresponding analysis, hope can be used for state reserves the right posture help.

(Full text)