Add ListenableBuilder with examples (#116543)
* Add ListenableBuilder with examples * Add tests * Add tests * Fix Test * Change AnimatedBuilder to be a subclass of ListenableBuilder
This commit is contained in:
parent
609fe35f13
commit
fb9133b881
172
examples/api/lib/widgets/transitions/listenable_builder.0.dart
Normal file
172
examples/api/lib/widgets/transitions/listenable_builder.0.dart
Normal file
@ -0,0 +1,172 @@
|
||||
// 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.
|
||||
|
||||
/// Flutter code sample for [ListenableBuilder].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const ListenableBuilderExample());
|
||||
|
||||
/// This widget listens for changes in the focus state of the subtree defined by
|
||||
/// its [child] widget, changing the border and color of the container it is in
|
||||
/// when it has focus.
|
||||
///
|
||||
/// A [FocusListenerContainer] swaps out the [BorderSide] of a border around the
|
||||
/// child widget with [focusedSide], and the background color with
|
||||
/// [focusedColor], when a widget that is a descendant of this widget has focus.
|
||||
class FocusListenerContainer extends StatefulWidget {
|
||||
const FocusListenerContainer({
|
||||
super.key,
|
||||
this.border,
|
||||
this.padding,
|
||||
this.focusedSide,
|
||||
this.focusedColor = Colors.black12,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// This is the border that will be used when not focused, and which defines
|
||||
/// all the attributes except for the [OutlinedBorder.side] when focused.
|
||||
final OutlinedBorder? border;
|
||||
|
||||
/// This is the [BorderSide] that will be used for [border] when the [child]
|
||||
/// subtree is focused.
|
||||
final BorderSide? focusedSide;
|
||||
|
||||
/// This is the [Color] that will be used as the fill color for the background
|
||||
/// of the [child] when a descendant widget is focused.
|
||||
final Color? focusedColor;
|
||||
|
||||
/// The padding around the inside of the container.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// This is defines the subtree to listen to for focus changes.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<FocusListenerContainer> createState() => _FocusListenerContainerState();
|
||||
}
|
||||
|
||||
class _FocusListenerContainerState extends State<FocusListenerContainer> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final OutlinedBorder effectiveBorder = widget.border ?? const RoundedRectangleBorder();
|
||||
return ListenableBuilder(
|
||||
listenable: _focusNode,
|
||||
child: Focus(
|
||||
focusNode: _focusNode,
|
||||
skipTraversal: true,
|
||||
canRequestFocus: false,
|
||||
child: widget.child,
|
||||
),
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Container(
|
||||
padding: widget.padding,
|
||||
decoration: ShapeDecoration(
|
||||
color: _focusNode.hasFocus ? widget.focusedColor : null,
|
||||
shape: effectiveBorder.copyWith(
|
||||
side: _focusNode.hasFocus ? widget.focusedSide : null,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyField extends StatefulWidget {
|
||||
const MyField({super.key, required this.label});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
State<MyField> createState() => _MyFieldState();
|
||||
}
|
||||
|
||||
class _MyFieldState extends State<MyField> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: Text(widget.label)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onEditingComplete: () {
|
||||
debugPrint('Field ${widget.label} changed to ${controller.value}');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListenableBuilderExample extends StatelessWidget {
|
||||
const ListenableBuilderExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('ListenableBuilder Example')),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: MyField(label: 'Company'),
|
||||
),
|
||||
FocusListenerContainer(
|
||||
padding: const EdgeInsets.all(8),
|
||||
border: const RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
// The border side will get wider when the subtree has focus.
|
||||
focusedSide: const BorderSide(
|
||||
width: 4,
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
),
|
||||
// The container background will change color to this when
|
||||
// the subtree has focus.
|
||||
focusedColor: Colors.blue.shade50,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const <Widget>[
|
||||
Text('Owner:'),
|
||||
MyField(label: 'First Name'),
|
||||
MyField(label: 'Last Name'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,10 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// Flutter code sample for a [ChangeNotifier] with an [AnimatedBuilder].
|
||||
/// Flutter code sample for a [ChangeNotifier] with a [ListenableBuilder].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() { runApp(const ListenableBuilderExample()); }
|
||||
|
||||
class CounterBody extends StatelessWidget {
|
||||
const CounterBody({super.key, required this.counterValueNotifier});
|
||||
|
||||
@ -18,13 +20,12 @@ class CounterBody extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('Current counter value:'),
|
||||
// Thanks to the [AnimatedBuilder], only the widget displaying the
|
||||
// current count is rebuilt when `counterValueNotifier` notifies its
|
||||
// listeners. The [Text] widget above and [CounterBody] itself aren't
|
||||
// Thanks to the ListenableBuilder, only the widget displaying the
|
||||
// current count is rebuilt when counterValueNotifier notifies its
|
||||
// listeners. The Text widget above and CounterBody itself aren't
|
||||
// rebuilt.
|
||||
AnimatedBuilder(
|
||||
// [AnimatedBuilder] accepts any [Listenable] subtype.
|
||||
animation: counterValueNotifier,
|
||||
ListenableBuilder(
|
||||
listenable: counterValueNotifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Text('${counterValueNotifier.value}');
|
||||
},
|
||||
@ -35,21 +36,21 @@ class CounterBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
class ListenableBuilderExample extends StatefulWidget {
|
||||
const ListenableBuilderExample({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
State<ListenableBuilderExample> createState() => _ListenableBuilderExampleState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
class _ListenableBuilderExampleState extends State<ListenableBuilderExample> {
|
||||
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('AnimatedBuilder example')),
|
||||
appBar: AppBar(title: const Text('ListenableBuilder Example')),
|
||||
body: CounterBody(counterValueNotifier: _counter),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _counter.value++,
|
||||
@ -59,7 +60,3 @@ class _MyAppState extends State<MyApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// 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/foundation/change_notifier/change_notifier.0.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Smoke test for MyApp', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
expect(find.byType(Scaffold), findsOneWidget);
|
||||
expect(find.byType(CounterBody), findsOneWidget);
|
||||
expect(find.byType(FloatingActionButton), findsOneWidget);
|
||||
expect(find.text('Current counter value:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Counter update', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Initial state of the counter
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
||||
// Tapping the increase button
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Counter should be at 1
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text('0'), findsNothing);
|
||||
});
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
// 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/transitions/listenable_builder.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Changing focus changes border', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.ListenableBuilderExample());
|
||||
|
||||
Finder findContainer() => find.descendant(of: find.byType(example.FocusListenerContainer), matching: find.byType(Container)).first;
|
||||
Finder findChild() => find.descendant(of: findContainer(), matching: find.byType(Column)).first;
|
||||
bool childHasFocus() => Focus.of(tester.element(findChild())).hasFocus;
|
||||
Container getContainer() => tester.widget(findContainer()) as Container;
|
||||
ShapeDecoration getDecoration() => getContainer().decoration! as ShapeDecoration;
|
||||
OutlinedBorder getBorder() => getDecoration().shape as OutlinedBorder;
|
||||
|
||||
expect(find.text('Company'), findsOneWidget);
|
||||
expect(find.text('First Name'), findsOneWidget);
|
||||
expect(find.text('Last Name'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(TextField).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(childHasFocus(), isFalse);
|
||||
expect(getBorder().side.width, equals(1));
|
||||
expect(getContainer().color, isNull);
|
||||
expect(getDecoration().color, isNull);
|
||||
|
||||
await tester.tap(find.byType(TextField).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(childHasFocus(), isTrue);
|
||||
expect(getBorder().side.width, equals(4));
|
||||
expect(getDecoration().color, equals(Colors.blue.shade50));
|
||||
|
||||
await tester.tap(find.byType(TextField).at(2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(childHasFocus(), isTrue);
|
||||
expect(getBorder().side.width, equals(4));
|
||||
expect(getDecoration().color, equals(Colors.blue.shade50));
|
||||
});
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// 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/transitions/listenable_builder.1.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Tapping FAB increments counter', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.ListenableBuilderExample());
|
||||
|
||||
String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder), matching: find.byType(Text))) as Text).data!;
|
||||
|
||||
expect(find.text('Current counter value:'), findsOneWidget);
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.add), findsOneWidget);
|
||||
expect(getCount(), equals('0'));
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(getCount(), equals('1'));
|
||||
});
|
||||
}
|
@ -104,7 +104,7 @@ const String _flutterFoundationLibrary = 'package:flutter/foundation.dart';
|
||||
/// It is O(1) for adding listeners and O(N) for removing listeners and dispatching
|
||||
/// notifications (where N is the number of listeners).
|
||||
///
|
||||
/// {@macro flutter.flutter.animatedbuilder_changenotifier.rebuild}
|
||||
/// {@macro flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -4910,8 +4910,8 @@ typedef NullableIndexedWidgetBuilder = Widget? Function(BuildContext context, in
|
||||
///
|
||||
/// The child should typically be part of the returned widget tree.
|
||||
///
|
||||
/// Used by [AnimatedBuilder.builder], as well as [WidgetsApp.builder] and
|
||||
/// [MaterialApp.builder].
|
||||
/// Used by [AnimatedBuilder.builder], [ListenableBuilder.builder],
|
||||
/// [WidgetsApp.builder], and [MaterialApp.builder].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -32,7 +32,7 @@ export 'package:flutter/rendering.dart' show RelativeRect;
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// For more complex case involving additional state, consider using
|
||||
/// [AnimatedBuilder].
|
||||
/// [AnimatedBuilder] or [ListenableBuilder].
|
||||
///
|
||||
/// ## Relationship to [ImplicitlyAnimatedWidget]s
|
||||
///
|
||||
@ -55,8 +55,10 @@ export 'package:flutter/rendering.dart' show RelativeRect;
|
||||
/// with subclasses of [ImplicitlyAnimatedWidget] (see above), which are usually
|
||||
/// named `AnimatedFoo`. Commonly used animated widgets include:
|
||||
///
|
||||
/// * [AnimatedBuilder], which is useful for complex animation use cases and a
|
||||
/// notable exception to the naming scheme of [AnimatedWidget] subclasses.
|
||||
/// * [ListenableBuilder], which uses a builder pattern that is useful for
|
||||
/// complex [Listenable] use cases.
|
||||
/// * [AnimatedBuilder], which uses a builder pattern that is useful for
|
||||
/// complex [Animation] use cases.
|
||||
/// * [AlignTransition], which is an animated version of [Align].
|
||||
/// * [DecoratedBoxTransition], which is an animated version of [DecoratedBox].
|
||||
/// * [DefaultTextStyleTransition], which is an animated version of
|
||||
@ -97,7 +99,7 @@ abstract class AnimatedWidget extends StatefulWidget {
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
|
||||
properties.add(DiagnosticsProperty<Listenable>('listenable', listenable));
|
||||
}
|
||||
}
|
||||
|
||||
@ -996,10 +998,108 @@ class DefaultTextStyleTransition extends AnimatedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A general-purpose widget for building a widget subtree when a [Listenable]
|
||||
/// changes.
|
||||
///
|
||||
/// [ListenableBuilder] is useful for more complex widgets that wish to listen
|
||||
/// to changes in other objects as part of a larger build function. To use
|
||||
/// [ListenableBuilder], simply construct the widget and pass it a [builder]
|
||||
/// function.
|
||||
///
|
||||
/// Any subtype of [Listenable] (such as a [ChangeNotifier], [ValueNotifier], or
|
||||
/// [Animation]) can be used with a [ListenableBuilder] to rebuild only certain
|
||||
/// parts of a widget when the [Listenable] notifies its listeners. Although
|
||||
/// they have identical implementations, if an [Animation] is being listened to,
|
||||
/// consider using an [AnimatedBuilder] instead for better readability.
|
||||
///
|
||||
/// ## Performance optimizations
|
||||
///
|
||||
/// {@template flutter.widgets.transitions.ListenableBuilder.optimizations}
|
||||
/// If the [builder] function contains a subtree that does not depend on the
|
||||
/// [listenable], it's often more efficient to build that subtree once instead
|
||||
/// of rebuilding it on every change of the listenable.
|
||||
///
|
||||
/// If a pre-built subtree is passed as the [child] parameter, the
|
||||
/// [ListenableBuilder] will pass it back to the [builder] function so that it
|
||||
/// can be incorporated into the build.
|
||||
///
|
||||
/// Using this pre-built [child] is entirely optional, but can improve
|
||||
/// performance significantly in some cases and is therefore a good practice.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how a [ListenableBuilder] can be used to listen to a
|
||||
/// [FocusNode] (which is also a [ChangeNotifier]) to see when a subtree has
|
||||
/// focus, and modify a decoration when its focus state changes.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@template flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild}
|
||||
/// ## Improve rebuild performance
|
||||
///
|
||||
/// Performance can be improved by specifying any widgets that don't need to
|
||||
/// change as a result of changes in the listener as the prebuilt
|
||||
/// [ListenableBuilder.child] attribute.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// The following example implements a simple counter that utilizes a
|
||||
/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing
|
||||
/// the count. The current count is stored in a [ValueNotifier], which rebuilds
|
||||
/// the [ListenableBuilder]'s contents when its value is changed.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.1.dart **
|
||||
/// {@end-tool}
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedBuilder], which has the same functionality, but is named more
|
||||
/// appropriately for a builder triggered by [Animation]s.
|
||||
class ListenableBuilder extends AnimatedWidget {
|
||||
/// Creates a builder that responds to changes in [listenable].
|
||||
///
|
||||
/// The [listenable] and [builder] arguments must not be null.
|
||||
const ListenableBuilder({
|
||||
super.key,
|
||||
required super.listenable,
|
||||
required this.builder,
|
||||
this.child,
|
||||
});
|
||||
|
||||
// Overridden getter to replace with documentation tailored to
|
||||
// ListenableBuilder.
|
||||
|
||||
/// The [Listenable] supplied to the constructor.
|
||||
///
|
||||
/// Also accessible through the [listenable] getter.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedBuilder], a widget with an identical functionality commonly
|
||||
/// used with [Animation] [Listenable]s for better readability.
|
||||
@override
|
||||
Listenable get listenable => super.listenable;
|
||||
|
||||
/// Called every time the [listenable] notifies about a change.
|
||||
///
|
||||
/// The child given to the builder should typically be part of the returned
|
||||
/// widget tree.
|
||||
final TransitionBuilder builder;
|
||||
|
||||
/// The child widget to pass to the [builder].
|
||||
///
|
||||
/// {@macro flutter.widgets.transitions.ListenableBuilder.optimizations}
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => builder(context, child);
|
||||
}
|
||||
|
||||
/// A general-purpose widget for building animations.
|
||||
///
|
||||
/// AnimatedBuilder is useful for more complex widgets that wish to include
|
||||
/// an animation as part of a larger build function. To use AnimatedBuilder,
|
||||
/// [AnimatedBuilder] is useful for more complex widgets that wish to include
|
||||
/// an animation as part of a larger build function. To use [AnimatedBuilder],
|
||||
/// simply construct the widget and pass it a builder function.
|
||||
///
|
||||
/// For simple cases without additional state, consider using
|
||||
@ -1009,16 +1109,18 @@ class DefaultTextStyleTransition extends AnimatedWidget {
|
||||
///
|
||||
/// ## Performance optimizations
|
||||
///
|
||||
/// If your [builder] function contains a subtree that does not depend on the
|
||||
/// animation, it's more efficient to build that subtree once instead of
|
||||
/// rebuilding it on every animation tick.
|
||||
/// {@template flutter.widgets.transitions.AnimatedBuilder.optimizations}
|
||||
/// If the [builder] function contains a subtree that does not depend on the
|
||||
/// animation passed to the constructor, it's more efficient to build that
|
||||
/// subtree once instead of rebuilding it on every animation tick.
|
||||
///
|
||||
/// If you pass the pre-built subtree as the [child] parameter, the
|
||||
/// [AnimatedBuilder] will pass it back to your builder function so that you
|
||||
/// can incorporate it into your build.
|
||||
/// If a pre-built subtree is passed as the [child] parameter, the
|
||||
/// [AnimatedBuilder] will pass it back to the [builder] function so that it can
|
||||
/// be incorporated into the build.
|
||||
///
|
||||
/// Using this pre-built child is entirely optional, but can improve
|
||||
/// performance significantly in some cases and is therefore a good practice.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This code defines a widget that spins a green square continually. It is
|
||||
@ -1028,61 +1130,64 @@ class DefaultTextStyleTransition extends AnimatedWidget {
|
||||
/// ** See code in examples/api/lib/widgets/transitions/animated_builder.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@template flutter.flutter.animatedbuilder_changenotifier.rebuild}
|
||||
/// ## Improve rebuilds performance using AnimatedBuilder
|
||||
/// ## Improve rebuild performance
|
||||
///
|
||||
/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s. Any subtype
|
||||
/// of [Listenable] (such as [ChangeNotifier] and [ValueNotifier]) can be used with
|
||||
/// an [AnimatedBuilder] to rebuild only certain parts of a widget when the
|
||||
/// [Listenable] notifies its listeners. This technique is a performance improvement
|
||||
/// that allows rebuilding only specific widgets leaving others untouched.
|
||||
/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s, any
|
||||
/// subtype of [Listenable] (such as [ChangeNotifier] or [ValueNotifier]) can be
|
||||
/// used to trigger rebuilds. Although they have identical implementations, if
|
||||
/// an [Animation] is not being listened to, consider using a
|
||||
/// [ListenableBuilder] for better readability.
|
||||
///
|
||||
/// You can use an [AnimatedBuilder] or [ListenableBuilder] to rebuild only
|
||||
/// certain parts of a widget when the [Listenable] notifies its listeners. You
|
||||
/// can improve performance by specifying any widgets that don't need to change
|
||||
/// as a result of changes in the listener as the prebuilt [child] attribute.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// The following example implements a simple counter that utilizes an
|
||||
/// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current count
|
||||
/// is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s contents
|
||||
/// when its value is changed.
|
||||
/// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current
|
||||
/// count is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s
|
||||
/// contents when its value is changed.
|
||||
///
|
||||
/// ** See code in examples/api/lib/foundation/change_notifier/change_notifier.0.dart **
|
||||
/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.1.dart **
|
||||
/// {@end-tool}
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListenableBuilder], a widget with similar functionality, but is named
|
||||
/// more appropriately for a builder triggered on changes in [Listenable]s
|
||||
/// that aren't [Animation]s.
|
||||
/// * [TweenAnimationBuilder], which animates a property to a target value
|
||||
/// without requiring manual management of an [AnimationController].
|
||||
class AnimatedBuilder extends AnimatedWidget {
|
||||
class AnimatedBuilder extends ListenableBuilder {
|
||||
/// Creates an animated builder.
|
||||
///
|
||||
/// The [animation] and [builder] arguments must not be null.
|
||||
/// The [animation] and [builder] arguments are required.
|
||||
const AnimatedBuilder({
|
||||
super.key,
|
||||
required Listenable animation,
|
||||
required this.builder,
|
||||
this.child,
|
||||
}) : assert(animation != null),
|
||||
assert(builder != null),
|
||||
super(listenable: animation);
|
||||
required super.builder,
|
||||
super.child,
|
||||
}) : super(listenable: animation);
|
||||
|
||||
/// Called every time the animation changes value.
|
||||
final TransitionBuilder builder;
|
||||
/// The [Listenable] supplied to the constructor (typically an [Animation]).
|
||||
///
|
||||
/// Also accessible through the [listenable] getter.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListenableBuilder], a widget with similar functionality commonly used
|
||||
/// with [Listenable]s (such as [ChangeNotifier]) for better readability
|
||||
/// when the [animation] isn't an [Animation].
|
||||
Listenable get animation => super.listenable;
|
||||
|
||||
/// The child widget to pass to the [builder].
|
||||
///
|
||||
/// If a [builder] callback's return value contains a subtree that does not
|
||||
/// depend on the animation, it's more efficient to build that subtree once
|
||||
/// instead of rebuilding it on every animation tick.
|
||||
///
|
||||
/// If the pre-built subtree is passed as the [child] parameter, the
|
||||
/// [AnimatedBuilder] will pass it back to the [builder] function so that it
|
||||
/// can be incorporated into the build.
|
||||
///
|
||||
/// Using this pre-built child is entirely optional, but can improve
|
||||
/// performance significantly in some cases and is therefore a good practice.
|
||||
final Widget? child;
|
||||
// Overridden getter to replace with documentation tailored to
|
||||
// AnimatedBuilder.
|
||||
|
||||
/// Called every time the [animation] notifies about a change.
|
||||
///
|
||||
/// The child given to the builder should typically be part of the returned
|
||||
/// widget tree.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return builder(context, child);
|
||||
}
|
||||
TransitionBuilder get builder => super.builder;
|
||||
}
|
||||
|
@ -562,4 +562,149 @@ void main() {
|
||||
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>())));
|
||||
});
|
||||
});
|
||||
|
||||
group('Builders', () {
|
||||
testWidgets('AnimatedBuilder rebuilds when changed', (WidgetTester tester) async {
|
||||
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
|
||||
final ChangeNotifier notifier = ChangeNotifier();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: AnimatedBuilder(
|
||||
animation: notifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return RedrawCounter(key: redrawKey, child: child);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
notifier.notifyListeners();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
|
||||
// Pump a few more times to make sure that we don't rebuild unnecessarily.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
});
|
||||
|
||||
testWidgets("AnimatedBuilder doesn't rebuild the child", (WidgetTester tester) async {
|
||||
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
|
||||
final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>();
|
||||
final ChangeNotifier notifier = ChangeNotifier();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: AnimatedBuilder(
|
||||
animation: notifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return RedrawCounter(key: redrawKey, child: child);
|
||||
},
|
||||
child: RedrawCounter(key: redrawKeyChild),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
notifier.notifyListeners();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
|
||||
// Pump a few more times to make sure that we don't rebuild unnecessarily.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
});
|
||||
|
||||
testWidgets('ListenableBuilder rebuilds when changed', (WidgetTester tester) async {
|
||||
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
|
||||
final ChangeNotifier notifier = ChangeNotifier();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListenableBuilder(
|
||||
listenable: notifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return RedrawCounter(key: redrawKey, child: child);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
notifier.notifyListeners();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
|
||||
// Pump a few more times to make sure that we don't rebuild unnecessarily.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
});
|
||||
|
||||
testWidgets("ListenableBuilder doesn't rebuild the child", (WidgetTester tester) async {
|
||||
final GlobalKey<RedrawCounterState> redrawKey = GlobalKey<RedrawCounterState>();
|
||||
final GlobalKey<RedrawCounterState> redrawKeyChild = GlobalKey<RedrawCounterState>();
|
||||
final ChangeNotifier notifier = ChangeNotifier();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListenableBuilder(
|
||||
listenable: notifier,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return RedrawCounter(key: redrawKey, child: child);
|
||||
},
|
||||
child: RedrawCounter(key: redrawKeyChild),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(1));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
notifier.notifyListeners();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
|
||||
// Pump a few more times to make sure that we don't rebuild unnecessarily.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(redrawKey.currentState!.redraws, equals(2));
|
||||
expect(redrawKeyChild.currentState!.redraws, equals(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class RedrawCounter extends StatefulWidget {
|
||||
const RedrawCounter({ super.key, this.child });
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
State<RedrawCounter> createState() => RedrawCounterState();
|
||||
}
|
||||
|
||||
class RedrawCounterState extends State<RedrawCounter> {
|
||||
int redraws = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
redraws += 1;
|
||||
return SizedBox(child: widget.child);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user