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: