Add documentation examples to KeepAlive AutomaticKeepAlive and AutomaticKeepAliveClientMixin (#168137)

Part of https://github.com/flutter/flutter/issues/153860



https://github.com/user-attachments/assets/8fd233da-d1b1-417a-8854-8d16c3ad195b



https://github.com/user-attachments/assets/93197949-b962-483e-bb8f-02db5206674c



https://github.com/user-attachments/assets/ad4f4091-6ed3-4a13-bcf3-f8572ce87481



## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Valentin Vignal 2025-05-02 15:03:35 +08:00 committed by GitHub
parent c795935606
commit c6ceffa2e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 436 additions and 0 deletions

View File

@ -0,0 +1,69 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [AutomaticKeepAlive].
///
/// This example demonstrates how to use the [AutomaticKeepAlive] to preserve the state
/// of individual list items in a `ListView` when they are scrolled out of view.
/// Each item has a counter that maintains its state.
void main() {
runApp(const AutomaticKeepAliveExampleApp());
}
class AutomaticKeepAliveExampleApp extends StatelessWidget {
const AutomaticKeepAliveExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('AutomaticKeepAlive Example')),
body: ListView.builder(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return AutomaticKeepAlive(child: _KeepAliveItem(index: index));
},
),
),
);
}
}
class _KeepAliveItem extends StatefulWidget {
const _KeepAliveItem({required this.index});
final int index;
@override
State<_KeepAliveItem> createState() => _KeepAliveItemState();
}
class _KeepAliveItemState extends State<_KeepAliveItem>
with AutomaticKeepAliveClientMixin<_KeepAliveItem> {
int _counter = 0;
@override
bool get wantKeepAlive => widget.index.isEven;
@override
Widget build(BuildContext context) {
super.build(context);
return ListTile(
title: Text('Item ${widget.index}: $_counter'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
_counter++;
});
},
),
);
}
}

View File

@ -0,0 +1,102 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [AutomaticKeepAliveClientMixin].
///
/// This example demonstrates how to use the [AutomaticKeepAliveClientMixin] to
/// preserve the state of individual list items in a `ListView` when they are
/// scrolled out of view. Each item has a counter that maintains its state.
void main() {
runApp(const AutomaticKeepAliveClientMixinExampleApp());
}
class AutomaticKeepAliveClientMixinExampleApp extends StatefulWidget {
const AutomaticKeepAliveClientMixinExampleApp({super.key});
@override
State<AutomaticKeepAliveClientMixinExampleApp> createState() =>
_AutomaticKeepAliveClientMixinExampleAppState();
}
class _AutomaticKeepAliveClientMixinExampleAppState
extends State<AutomaticKeepAliveClientMixinExampleApp> {
bool _keepAlive = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('AutomaticKeepAliveClientMixin Example'),
actions: <Widget>[
Row(
children: <Widget>[
const Text('Keep Alive'),
Switch(
value: _keepAlive,
onChanged: (bool value) {
setState(() {
_keepAlive = value;
});
},
),
],
),
],
),
body: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return _KeepAliveItem(index: index, keepAlive: _keepAlive);
},
),
),
);
}
}
class _KeepAliveItem extends StatefulWidget {
const _KeepAliveItem({required this.index, required this.keepAlive});
final int index;
final bool keepAlive;
@override
State<_KeepAliveItem> createState() => _KeepAliveItemState();
}
class _KeepAliveItemState extends State<_KeepAliveItem>
with AutomaticKeepAliveClientMixin<_KeepAliveItem> {
int _counter = 0;
@override
void didUpdateWidget(_KeepAliveItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.keepAlive != widget.keepAlive) {
updateKeepAlive();
}
}
@override
bool get wantKeepAlive => widget.keepAlive;
@override
Widget build(BuildContext context) {
super.build(context);
return ListTile(
title: Text('Item ${widget.index}: $_counter'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
_counter++;
});
},
),
);
}
}

View File

@ -0,0 +1,64 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [KeepAlive].
///
/// This example demonstrates how to use the [KeepAlive] to preserve the state
/// of individual list items in a `ListView` when they are scrolled out of view.
/// Each item has a counter that maintains its state.
void main() {
runApp(const KeepAliveExampleApp());
}
class KeepAliveExampleApp extends StatelessWidget {
const KeepAliveExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('KeepAlive Example')),
body: ListView.builder(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
addSemanticIndexes: false,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return KeepAlive(keepAlive: index.isEven, child: _KeepAliveItem(index: index));
},
),
),
);
}
}
class _KeepAliveItem extends StatefulWidget {
const _KeepAliveItem({required this.index});
final int index;
@override
State<_KeepAliveItem> createState() => _KeepAliveItemState();
}
class _KeepAliveItemState extends State<_KeepAliveItem> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('Item ${widget.index}: $_counter'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
_counter++;
});
},
),
);
}
}

View File

@ -0,0 +1,40 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/keep_alive/automatic_keep_alive.0.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('The state is maintained for the even items', (WidgetTester tester) async {
await tester.pumpWidget(const AutomaticKeepAliveExampleApp());
expect(find.text('Item 0: 0'), findsOne);
expect(find.text('Item 1: 0'), findsOne);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).first);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).at(1));
await tester.pump();
expect(find.text('Item 0: 1'), findsOne);
expect(find.text('Item 1: 1'), findsOne);
// Scrolls all the way down to the bottom of the list.
await tester.fling(find.byType(ListView), const Offset(0, -6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 99: 0'), findsOne);
// Scrolls all the way back to the top of the list.
await tester.fling(find.byType(ListView), const Offset(0, 6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 0: 1'), findsOne, reason: 'The state of item 0 should be maintained');
expect(
find.text('Item 1: 0'),
findsOne,
reason: 'The state of item 1 should not be maintained',
);
});
}

View File

@ -0,0 +1,66 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('The state is maintained when the item is scrolled out of view', (
WidgetTester tester,
) async {
await tester.pumpWidget(const AutomaticKeepAliveClientMixinExampleApp());
expect(find.text('Item 0: 0'), findsOne);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).first);
await tester.pump();
expect(find.text('Item 0: 1'), findsOne);
// Scrolls all the way down to the bottom of the list.
await tester.fling(find.byType(ListView), const Offset(0, -6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 99: 0'), findsOne);
// Scrolls all the way back to the top of the list.
await tester.fling(find.byType(ListView), const Offset(0, 6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 0: 1'), findsOne, reason: 'The state of item 0 should be maintained');
});
testWidgets('The state is not maintained when the item is scrolled out of view', (
WidgetTester tester,
) async {
await tester.pumpWidget(const AutomaticKeepAliveClientMixinExampleApp());
await tester.tap(find.byType(Switch));
await tester.pumpAndSettle();
expect(find.text('Item 0: 0'), findsOne);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).first);
await tester.pump();
expect(find.text('Item 0: 1'), findsOne);
// Scrolls all the way down to the bottom of the list.
await tester.fling(find.byType(ListView), const Offset(0, -6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 99: 0'), findsOne);
// Scrolls all the way back to the top of the list.
await tester.fling(find.byType(ListView), const Offset(0, 6000), 1000);
await tester.pumpAndSettle();
expect(
find.text('Item 0: 0'),
findsOne,
reason: 'The state of item 0 should not be maintained',
);
});
}

View File

@ -0,0 +1,40 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/keep_alive/keep_alive.0.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('The state is maintained for the even items', (WidgetTester tester) async {
await tester.pumpWidget(const KeepAliveExampleApp());
expect(find.text('Item 0: 0'), findsOne);
expect(find.text('Item 1: 0'), findsOne);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).first);
await tester.tap(find.widgetWithIcon(IconButton, Icons.add).at(1));
await tester.pump();
expect(find.text('Item 0: 1'), findsOne);
expect(find.text('Item 1: 1'), findsOne);
// Scrolls all the way down to the bottom of the list.
await tester.fling(find.byType(ListView), const Offset(0, -6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 99: 0'), findsOne);
// Scrolls all the way back to the top of the list.
await tester.fling(find.byType(ListView), const Offset(0, 6000), 1000);
await tester.pumpAndSettle();
expect(find.text('Item 0: 1'), findsOne, reason: 'The state of item 0 should be maintained');
expect(
find.text('Item 1: 0'),
findsOne,
reason: 'The state of item 1 should not be maintained',
);
});
}

View File

@ -26,6 +26,30 @@ import 'sliver.dart';
/// [KeepAliveNotification.handle].
///
/// To send these notifications, consider using [AutomaticKeepAliveClientMixin].
///
/// {@tool dartpad}
/// This sample demonstrates how to use the [AutomaticKeepAlive] widget in
/// combination with the [AutomaticKeepAliveClientMixin] to selectively preserve
/// the state of individual items in a scrollable list.
///
/// Normally, widgets in a lazily built list like [ListView.builder] are
/// disposed of when they leave the visible area to save resources. This means
/// that any state inside a [StatefulWidget] would be lost unless explicitly
/// preserved.
///
/// In this example, each list item is a [StatefulWidget] that includes a
/// counter and an increment button. To preserve the state of selected items
/// (based on their index), the [AutomaticKeepAlive] widget and
/// [AutomaticKeepAliveClientMixin] are used:
///
/// - The `wantKeepAlive` getter in the items state class returns true for
/// even-indexed items, indicating that their state should be preserved.
/// - For odd-indexed items, `wantKeepAlive` returns false, so their state is
/// not preserved when scrolled out of view.
///
/// ** See code in examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart **
/// {@end-tool}
///
class AutomaticKeepAlive extends StatefulWidget {
/// Creates a widget that listens to [KeepAliveNotification]s and maintains a
/// [KeepAlive] widget appropriately.
@ -342,6 +366,13 @@ class KeepAliveHandle extends ChangeNotifier {
/// The type argument `T` is the type of the [StatefulWidget] subclass of the
/// [State] into which this class is being mixed.
///
/// {@tool dartpad}
/// This example demonstrates how to use the [AutomaticKeepAliveClientMixin]
/// to keep the state of a widget alive even when it is scrolled out of view.
///
/// ** See code in examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [AutomaticKeepAlive], which listens to messages from this mixin.

View File

@ -1464,6 +1464,30 @@ class _SliverOffstageElement extends SingleChildRenderObjectElement {
/// In practice, the simplest way to deal with these notifications is to mix
/// [AutomaticKeepAliveClientMixin] into one's [State]. See the documentation
/// for that mixin class for details.
///
/// {@tool dartpad}
/// This sample demonstrates how to use the [KeepAlive] widget
/// to preserve the state of individual list items in a [ListView] when they are
/// scrolled out of view.
///
/// By default, [ListView.builder] only keeps the widgets currently visible in
/// the viewport alive. When an item scrolls out of view, it may be disposed to
/// free up resources. This can cause the state of [StatefulWidget]s to be lost
/// if not explicitly preserved.
///
/// In this example, each item in the list is a [StatefulWidget] that maintains
/// a counter. Tapping the "+" button increments the counter. To selectively
/// preserve the state, each item is wrapped in a [KeepAlive] widget, with the
/// keepAlive parameter set based on the items index:
///
/// - For even-indexed items, `keepAlive: true`, so their state is preserved
/// even when scrolled off-screen.
/// - For odd-indexed items, `keepAlive: false`, so their state is discarded
/// when they are no longer visible.
///
/// ** See code in examples/api/lib/widgets/keep_alive/keep_alive.0.dart **
/// {@end-tool}
///
class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
/// Marks a child as needing to remain alive.
const KeepAlive({super.key, required this.keepAlive, required super.child});