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
|
// 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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// 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
|
|||||||
///
|
///
|
||||||
/// 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;
|
|||||||
/// {@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;
|
|||||||
/// 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 {
|
|||||||
@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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
|||||||
///
|
///
|
||||||
/// ## 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 {
|
|||||||
/// ** 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
|
/// Despite the name, [AnimatedBuilder] is not limited to [Animation]s, any
|
||||||
/// of [Listenable] (such as [ChangeNotifier] and [ValueNotifier]) can be used with
|
/// subtype of [Listenable] (such as [ChangeNotifier] or [ValueNotifier]) can be
|
||||||
/// an [AnimatedBuilder] to rebuild only certain parts of a widget when the
|
/// used to trigger rebuilds. Although they have identical implementations, if
|
||||||
/// [Listenable] notifies its listeners. This technique is a performance improvement
|
/// an [Animation] is not being listened to, consider using a
|
||||||
/// that allows rebuilding only specific widgets leaving others untouched.
|
/// [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}
|
/// {@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.
|
/// The [Listenable] supplied to the constructor (typically an [Animation]).
|
||||||
final TransitionBuilder builder;
|
///
|
||||||
|
/// 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].
|
// Overridden getter to replace with documentation tailored to
|
||||||
///
|
// AnimatedBuilder.
|
||||||
/// 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;
|
|
||||||
|
|
||||||
|
/// 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() {
|
|||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user