Unverified Commit fb9133b8 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add ListenableBuilder with examples (#116543)

* Add ListenableBuilder with examples

* Add tests

* Add tests

* Fix Test

* Change AnimatedBuilder to be a subclass of ListenableBuilder
parent 609fe35f
// 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 @@ ...@@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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'; import 'package:flutter/material.dart';
void main() { runApp(const ListenableBuilderExample()); }
class CounterBody extends StatelessWidget { class CounterBody extends StatelessWidget {
const CounterBody({super.key, required this.counterValueNotifier}); const CounterBody({super.key, required this.counterValueNotifier});
...@@ -18,13 +20,12 @@ class CounterBody extends StatelessWidget { ...@@ -18,13 +20,12 @@ class CounterBody extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
const Text('Current counter value:'), const Text('Current counter value:'),
// Thanks to the [AnimatedBuilder], only the widget displaying the // Thanks to the ListenableBuilder, only the widget displaying the
// current count is rebuilt when `counterValueNotifier` notifies its // current count is rebuilt when counterValueNotifier notifies its
// listeners. The [Text] widget above and [CounterBody] itself aren't // listeners. The Text widget above and CounterBody itself aren't
// rebuilt. // rebuilt.
AnimatedBuilder( ListenableBuilder(
// [AnimatedBuilder] accepts any [Listenable] subtype. listenable: counterValueNotifier,
animation: counterValueNotifier,
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return Text('${counterValueNotifier.value}'); return Text('${counterValueNotifier.value}');
}, },
...@@ -35,21 +36,21 @@ class CounterBody extends StatelessWidget { ...@@ -35,21 +36,21 @@ class CounterBody extends StatelessWidget {
} }
} }
class MyApp extends StatefulWidget { class ListenableBuilderExample extends StatefulWidget {
const MyApp({super.key}); const ListenableBuilderExample({super.key});
@override @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); final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder example')), appBar: AppBar(title: const Text('ListenableBuilder Example')),
body: CounterBody(counterValueNotifier: _counter), body: CounterBody(counterValueNotifier: _counter),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _counter.value++, onPressed: () => _counter.value++,
...@@ -59,7 +60,3 @@ class _MyAppState extends State<MyApp> { ...@@ -59,7 +60,3 @@ class _MyAppState extends State<MyApp> {
); );
} }
} }
void main() {
runApp(const MyApp());
}
// 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));
});
}
...@@ -3,31 +3,22 @@ ...@@ -3,31 +3,22 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_api_samples/foundation/change_notifier/change_notifier.0.dart'; import 'package:flutter_api_samples/widgets/transitions/listenable_builder.1.dart' as example;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('Smoke test for MyApp', (WidgetTester tester) async { testWidgets('Tapping FAB increments counter', (WidgetTester tester) async {
await tester.pumpWidget(const MyApp()); await tester.pumpWidget(const example.ListenableBuilderExample());
expect(find.byType(Scaffold), findsOneWidget); String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder), matching: find.byType(Text))) as Text).data!;
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('Current counter value:'), findsOneWidget);
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(find.byIcon(Icons.add), findsOneWidget);
expect(getCount(), equals('0'));
// Tapping the increase button await tester.tap(find.byType(FloatingActionButton).first);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(getCount(), equals('1'));
// Counter should be at 1
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
}); });
} }
...@@ -104,7 +104,7 @@ const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; ...@@ -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 /// It is O(1) for adding listeners and O(N) for removing listeners and dispatching
/// notifications (where N is the number of listeners). /// notifications (where N is the number of listeners).
/// ///
/// {@macro flutter.flutter.animatedbuilder_changenotifier.rebuild} /// {@macro flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild}
/// ///
/// See also: /// See also:
/// ///
......
...@@ -4910,14 +4910,14 @@ typedef NullableIndexedWidgetBuilder = Widget? Function(BuildContext context, in ...@@ -4910,14 +4910,14 @@ typedef NullableIndexedWidgetBuilder = Widget? Function(BuildContext context, in
/// ///
/// The child should typically be part of the returned widget tree. /// The child should typically be part of the returned widget tree.
/// ///
/// Used by [AnimatedBuilder.builder], as well as [WidgetsApp.builder] and /// Used by [AnimatedBuilder.builder], [ListenableBuilder.builder],
/// [MaterialApp.builder]. /// [WidgetsApp.builder], and [MaterialApp.builder].
/// ///
/// See also: /// See also:
/// ///
/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. /// * [WidgetBuilder], which is similar but only takes a [BuildContext].
/// * [IndexedWidgetBuilder], which is similar but also takes an index. /// * [IndexedWidgetBuilder], which is similar but also takes an index.
/// * [ValueWidgetBuilder], which is similar but takes a value and a child. /// * [ValueWidgetBuilder], which is similar but takes a value and a child.
typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child); typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);
/// An [Element] that composes other [Element]s. /// An [Element] that composes other [Element]s.
......
...@@ -32,7 +32,7 @@ export 'package:flutter/rendering.dart' show RelativeRect; ...@@ -32,7 +32,7 @@ export 'package:flutter/rendering.dart' show RelativeRect;
/// {@end-tool} /// {@end-tool}
/// ///
/// For more complex case involving additional state, consider using /// For more complex case involving additional state, consider using
/// [AnimatedBuilder]. /// [AnimatedBuilder] or [ListenableBuilder].
/// ///
/// ## Relationship to [ImplicitlyAnimatedWidget]s /// ## Relationship to [ImplicitlyAnimatedWidget]s
/// ///
...@@ -55,8 +55,10 @@ export 'package:flutter/rendering.dart' show RelativeRect; ...@@ -55,8 +55,10 @@ export 'package:flutter/rendering.dart' show RelativeRect;
/// with subclasses of [ImplicitlyAnimatedWidget] (see above), which are usually /// with subclasses of [ImplicitlyAnimatedWidget] (see above), which are usually
/// named `AnimatedFoo`. Commonly used animated widgets include: /// named `AnimatedFoo`. Commonly used animated widgets include:
/// ///
/// * [AnimatedBuilder], which is useful for complex animation use cases and a /// * [ListenableBuilder], which uses a builder pattern that is useful for
/// notable exception to the naming scheme of [AnimatedWidget] subclasses. /// 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]. /// * [AlignTransition], which is an animated version of [Align].
/// * [DecoratedBoxTransition], which is an animated version of [DecoratedBox]. /// * [DecoratedBoxTransition], which is an animated version of [DecoratedBox].
/// * [DefaultTextStyleTransition], which is an animated version of /// * [DefaultTextStyleTransition], which is an animated version of
...@@ -97,7 +99,7 @@ abstract class AnimatedWidget extends StatefulWidget { ...@@ -97,7 +99,7 @@ abstract class AnimatedWidget extends StatefulWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Listenable>('animation', listenable)); properties.add(DiagnosticsProperty<Listenable>('listenable', listenable));
} }
} }
...@@ -996,10 +998,108 @@ class DefaultTextStyleTransition extends AnimatedWidget { ...@@ -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. /// A general-purpose widget for building animations.
/// ///
/// AnimatedBuilder is useful for more complex widgets that wish to include /// [AnimatedBuilder] is useful for more complex widgets that wish to include
/// an animation as part of a larger build function. To use AnimatedBuilder, /// an animation as part of a larger build function. To use [AnimatedBuilder],
/// simply construct the widget and pass it a builder function. /// simply construct the widget and pass it a builder function.
/// ///
/// For simple cases without additional state, consider using /// For simple cases without additional state, consider using
...@@ -1009,16 +1109,18 @@ class DefaultTextStyleTransition extends AnimatedWidget { ...@@ -1009,16 +1109,18 @@ class DefaultTextStyleTransition extends AnimatedWidget {
/// ///
/// ## Performance optimizations /// ## Performance optimizations
/// ///
/// If your [builder] function contains a subtree that does not depend on the /// {@template flutter.widgets.transitions.AnimatedBuilder.optimizations}
/// animation, it's more efficient to build that subtree once instead of /// If the [builder] function contains a subtree that does not depend on the
/// rebuilding it on every animation tick. /// 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 /// If a pre-built subtree is passed as the [child] parameter, the
/// [AnimatedBuilder] will pass it back to your builder function so that you /// [AnimatedBuilder] will pass it back to the [builder] function so that it can
/// can incorporate it into your build. /// be incorporated into the build.
/// ///
/// Using this pre-built child is entirely optional, but can improve /// Using this pre-built child is entirely optional, but can improve
/// performance significantly in some cases and is therefore a good practice. /// performance significantly in some cases and is therefore a good practice.
/// {@endtemplate}
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// This code defines a widget that spins a green square continually. It is /// This code defines a widget that spins a green square continually. It is
...@@ -1028,61 +1130,64 @@ class DefaultTextStyleTransition extends AnimatedWidget { ...@@ -1028,61 +1130,64 @@ class DefaultTextStyleTransition extends AnimatedWidget {
/// ** See code in examples/api/lib/widgets/transitions/animated_builder.0.dart ** /// ** See code in examples/api/lib/widgets/transitions/animated_builder.0.dart **
/// {@end-tool} /// {@end-tool}
/// ///
/// {@template flutter.flutter.animatedbuilder_changenotifier.rebuild} /// ## Improve rebuild performance
/// ## Improve rebuilds performance using AnimatedBuilder ///
/// 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.
/// ///
/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s. Any subtype /// You can use an [AnimatedBuilder] or [ListenableBuilder] to rebuild only
/// of [Listenable] (such as [ChangeNotifier] and [ValueNotifier]) can be used with /// certain parts of a widget when the [Listenable] notifies its listeners. You
/// an [AnimatedBuilder] to rebuild only certain parts of a widget when the /// can improve performance by specifying any widgets that don't need to change
/// [Listenable] notifies its listeners. This technique is a performance improvement /// as a result of changes in the listener as the prebuilt [child] attribute.
/// that allows rebuilding only specific widgets leaving others untouched.
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// The following example implements a simple counter that utilizes an /// The following example implements a simple counter that utilizes an
/// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current count /// [AnimatedBuilder] to limit rebuilds to only the [Text] widget. The current
/// is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s contents /// count is stored in a [ValueNotifier], which rebuilds the [AnimatedBuilder]'s
/// when its value is changed. /// 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} /// {@end-tool}
/// {@endtemplate}
/// ///
/// See also: /// See also:
/// ///
/// * [TweenAnimationBuilder], which animates a property to a target value /// * [ListenableBuilder], a widget with similar functionality, but is named
/// without requiring manual management of an [AnimationController]. /// more appropriately for a builder triggered on changes in [Listenable]s
class AnimatedBuilder extends AnimatedWidget { /// that aren't [Animation]s.
/// * [TweenAnimationBuilder], which animates a property to a target value
/// without requiring manual management of an [AnimationController].
class AnimatedBuilder extends ListenableBuilder {
/// Creates an animated builder. /// Creates an animated builder.
/// ///
/// The [animation] and [builder] arguments must not be null. /// The [animation] and [builder] arguments are required.
const AnimatedBuilder({ const AnimatedBuilder({
super.key, super.key,
required Listenable animation, required Listenable animation,
required this.builder, required super.builder,
this.child, super.child,
}) : assert(animation != null), }) : super(listenable: animation);
assert(builder != null),
super(listenable: animation);
/// Called every time the animation changes value.
final TransitionBuilder builder;
/// The child widget to pass to the [builder]. /// The [Listenable] supplied to the constructor (typically an [Animation]).
/// ///
/// If a [builder] callback's return value contains a subtree that does not /// Also accessible through the [listenable] getter.
/// 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 /// See also:
/// [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 /// * [ListenableBuilder], a widget with similar functionality commonly used
/// performance significantly in some cases and is therefore a good practice. /// with [Listenable]s (such as [ChangeNotifier]) for better readability
final Widget? child; /// when the [animation] isn't an [Animation].
Listenable get animation => super.listenable;
// 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 @override
Widget build(BuildContext context) { TransitionBuilder get builder => super.builder;
return builder(context, child);
}
} }
...@@ -562,4 +562,149 @@ void main() { ...@@ -562,4 +562,149 @@ void main() {
expect(tester.layers, isNot(contains(isA<ImageFilterLayer>()))); 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);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment