Skip to content

Commit 4fc78b9

Browse files
Fix a memory leak in AutomaticKeepAlive (#124163)
Fix a memory leak in `AutomaticKeepAlive`
1 parent 509c2dd commit 4fc78b9

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

packages/flutter/lib/src/widgets/automatic_keep_alive.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
144144
}
145145

146146
VoidCallback _createCallback(Listenable handle) {
147-
return () {
147+
late final VoidCallback callback;
148+
return callback = () {
148149
assert(() {
149150
if (!mounted) {
150151
throw FlutterError(
@@ -157,6 +158,7 @@ class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
157158
return true;
158159
}());
159160
_handles!.remove(handle);
161+
handle.removeListener(callback);
160162
if (_handles!.isEmpty) {
161163
if (SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.persistentCallbacks.index) {
162164
// Build/layout haven't started yet so let's just schedule this for

packages/flutter/test/widgets/automatic_keep_alive_test.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,26 @@ void main() {
557557

558558
expect(alternate.children.length, 1);
559559
});
560+
561+
testWidgets('Keep alive Listenable has its listener removed once called', (WidgetTester tester) async {
562+
final LeakCheckerHandle handle = LeakCheckerHandle();
563+
await tester.pumpWidget(Directionality(
564+
textDirection: TextDirection.ltr,
565+
child: ListView.builder(
566+
itemCount: 1,
567+
itemBuilder: (BuildContext context, int index) {
568+
return const KeepAliveListenableLeakChecker(key: GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0));
569+
},
570+
),
571+
));
572+
final _KeepAliveListenableLeakCheckerState state = const GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0).currentState!;
573+
574+
expect(handle.hasListeners, false);
575+
state.dispatch(handle);
576+
expect(handle.hasListeners, true);
577+
handle.notifyListeners();
578+
expect(handle.hasListeners, false);
579+
});
560580
}
561581

562582
class _AlwaysKeepAlive extends StatefulWidget {
@@ -633,3 +653,26 @@ class RenderSliverMultiBoxAdaptorAlt extends RenderSliver with
633653
@override
634654
void performLayout() { }
635655
}
656+
657+
class LeakCheckerHandle with ChangeNotifier {
658+
@override
659+
bool get hasListeners => super.hasListeners;
660+
}
661+
662+
class KeepAliveListenableLeakChecker extends StatefulWidget {
663+
const KeepAliveListenableLeakChecker({super.key});
664+
665+
@override
666+
State<KeepAliveListenableLeakChecker> createState() => _KeepAliveListenableLeakCheckerState();
667+
}
668+
669+
class _KeepAliveListenableLeakCheckerState extends State<KeepAliveListenableLeakChecker> {
670+
void dispatch(Listenable handle) {
671+
KeepAliveNotification(handle).dispatch(context);
672+
}
673+
674+
@override
675+
Widget build(BuildContext context) {
676+
return const Placeholder();
677+
}
678+
}

0 commit comments

Comments
 (0)