Note to Keep Alive

Note to Keep Alive

·

3 min read

Recently testing on the example of keeping image in ListView. Simply using AutomaticKeepAliveClientMixin can solve the case.

This is an image keeping example class.

class ImageKeepAliveWidget extends StatefulWidget {
  const ImageKeepAliveWidget({Key? key}) : super(key: key);

  @override
  State<ImageKeepAliveWidget> createState() => _ImageKeepAliveWidgetState();
}

/// use [AutomaticKeepAliveClientMixin] keeping the state
class _ImageKeepAliveWidgetState extends State<ImageKeepAliveWidget>
    with AutomaticKeepAliveClientMixin<ImageKeepAliveWidget> {
  String? _imageUrl;

  @override
  void initState() {
    super.initState();
    _fetchUrl();
  }

  void _fetchUrl() async {
    try {
      http.Response res =
          await http.get(Uri.parse('https://dog.ceo/api/breeds/image/random'));
      _imageUrl = json.decode(res.body)['message'];
      setState(() {});
    } catch (e) {
      debugPrint('error in getting image url');
    }
  }

  @override
  Widget build(BuildContext context) {
    /// must add super.build(context) to preserve state
    super.build(context);
    if (_imageUrl?.isNotEmpty == true) {
      return Image.network(_imageUrl!);
    } else {
      return const CircularProgressIndicator();
    }
  }

  @override
  bool get wantKeepAlive => true;
}

But how the mechanism is doing? Let’s take a look to the ListView first.

If you trace the ListView addAutomaticKeepAlives , you will find ListView children delegate is using SliverChildListDelegate. Inside its build function, you can see if the addAutomaticKeepAlives is true, the child widget will be wrapped by a AutomaticKeepAlive widget.

@override
  Widget? build(BuildContext context, int index) {
    /// ...
    if (addAutomaticKeepAlives)
      child = AutomaticKeepAlive(child: child);
    return KeyedSubtree(key: key, child: child);
  }

What will AutomaticKeepAlive do?

Checking the _AutomaticKeepAliveState, we can see the _updateChild is called when this stateful widget is updated. It wraps a NotificationListener for listening the KeepAliveNotification. And later it will wrap a KeepAlive widget for the listener widget.

@override
void initState() {
  super.initState();
  _updateChild();
}

@override
void didUpdateWidget(AutomaticKeepAlive oldWidget) {
  super.didUpdateWidget(oldWidget);
  _updateChild();
}

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

@override
Widget build(BuildContext context) {
  assert(_child != null);
  return KeepAlive(
    keepAlive: _keepingAlive,
    child: _child!,
  );
}

Let’s check what the notification callback has done in the listener widget.

In short, the callback function has 2 main goals. One is using a map to store the KeepAliveNotification handler. Another is to apply parent data to child which is the later KeepAlive widget.

bool _addClient(KeepAliveNotification notification) {
  final Listenable handle = notification.handle;
  _handles ??= <Listenable, VoidCallback>{};
  assert(!_handles!.containsKey(handle));
  _handles![handle] = _createCallback(handle);
  handle.addListener(_handles![handle]!);
  if (!_keepingAlive) {
    _keepingAlive = true;
    final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
    if (childElement != null) {
      // If the child already exists, update it synchronously.
      _updateParentDataOfChild(childElement);
    } else {
      // If the child doesn't exist yet, we got called during the very first
      // build of this subtree. Wait until the end of the frame to update
      // the child when the child is guaranteed to be present.
      SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
        if (!mounted) {
          return;
        }
        final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
        assert(childElement != null);
        _updateParentDataOfChild(childElement!);
      });
    }
  }
  return false;
}

ParentDataElement<KeepAliveParentDataMixin>? _getChildElement() {
    ///...
}

In the KeepAlive widget, we can see the applyParentData function about how the parent data will set the renderObject keepAlive status by the notification from the AutomaticKeepAliveClientMixin. If it is not keepAlive, the renderObject will redo the layout.

@override
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 is RenderObject && !keepAlive)
      targetParent.markNeedsLayout(); // No need to redo layout if it became true.
  }
}

In summary, the whole process will be like this:

keepalive_(1).png