diff --git a/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart b/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart new file mode 100644 index 0000000000000..6e26898754672 --- /dev/null +++ b/examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart @@ -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++; + }); + }, + ), + ); + } +} diff --git a/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart b/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart new file mode 100644 index 0000000000000..d800117b78e76 --- /dev/null +++ b/examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart @@ -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 createState() => + _AutomaticKeepAliveClientMixinExampleAppState(); +} + +class _AutomaticKeepAliveClientMixinExampleAppState + extends State { + bool _keepAlive = true; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('AutomaticKeepAliveClientMixin Example'), + actions: [ + Row( + children: [ + 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++; + }); + }, + ), + ); + } +} diff --git a/examples/api/lib/widgets/keep_alive/keep_alive.0.dart b/examples/api/lib/widgets/keep_alive/keep_alive.0.dart new file mode 100644 index 0000000000000..60d387a342790 --- /dev/null +++ b/examples/api/lib/widgets/keep_alive/keep_alive.0.dart @@ -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++; + }); + }, + ), + ); + } +} diff --git a/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart b/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart new file mode 100644 index 0000000000000..b7d65adad1ab7 --- /dev/null +++ b/examples/api/test/widgets/keep_alive/automatic_keep_alive.0_test.dart @@ -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', + ); + }); +} diff --git a/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart b/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart new file mode 100644 index 0000000000000..9aab50865024c --- /dev/null +++ b/examples/api/test/widgets/keep_alive/automatic_keep_alive_client_mixin.0_test.dart @@ -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', + ); + }); +} diff --git a/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart b/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart new file mode 100644 index 0000000000000..f5fde12e926d5 --- /dev/null +++ b/examples/api/test/widgets/keep_alive/keep_alive.0_test.dart @@ -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', + ); + }); +} diff --git a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart index f01e5f44148b8..4965e1b5d9376 100644 --- a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart +++ b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart @@ -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 item’s 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. diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index f6d14ebf789ae..b89cdaa03d4df 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -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 item’s 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 { /// Marks a child as needing to remain alive. const KeepAlive({super.key, required this.keepAlive, required super.child});