Create a ValueListenableBuilder (#19729)
This commit is contained in:
126
packages/flutter/lib/src/widgets/value_listenable_builder.dart
Normal file
126
packages/flutter/lib/src/widgets/value_listenable_builder.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2018 The Chromium 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/foundation.dart';
|
||||
|
||||
import 'framework.dart';
|
||||
|
||||
/// Builds a [Widget] when given a concrete value of a [ValueListenable<T>].
|
||||
///
|
||||
/// If the `child` parameter provided to the [ValueListenableBuilder] is not
|
||||
/// null, the same `child` widget is passed back to this [ValueWidgetBuilder]
|
||||
/// and should typically be incorporated in the returned widget tree.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ValueListenableBuilder], a widget which invokes this builder each time
|
||||
/// a [ValueListenable] changes value.
|
||||
typedef Widget ValueWidgetBuilder<T>(BuildContext context, T value, Widget child);
|
||||
|
||||
/// A widget whose content stays sync'ed with a [ValueListenable].
|
||||
///
|
||||
/// Given a [ValueListenable<T>] and a [builder] which builds widgets from
|
||||
/// concrete values of `T`, this class will automatically register itself as a
|
||||
/// listener of the [ValueListenable] and call the [builder] with updated values
|
||||
/// when the value changes.
|
||||
///
|
||||
/// ## Performance optimizations
|
||||
///
|
||||
/// If your [builder] function contains a subtree that does not depend on the
|
||||
/// value of the [ValueListenable], 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
|
||||
/// [ValueListenableBuilder] will pass it back to your [builder] function so
|
||||
/// that you can incorporate it into your build.
|
||||
///
|
||||
/// Using this pre-built child is entirely optional, but can improve
|
||||
/// performance significantly in some cases and is therefore a good practice.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AnimatedBuilder], which also triggers rebuilds from a [Listenable]
|
||||
/// without passing back a specific value from a [ValueListenable].
|
||||
/// * [NotificationListener], which lets you rebuild based on [Notification]
|
||||
/// coming from its descendent widgets rather than a [ValueListenable] that
|
||||
/// you have a direct reference to.
|
||||
/// * [StreamBuilder], where a builder can depend on a [Stream] rather than
|
||||
/// a [ValueListenable] for more advanced use cases.
|
||||
class ValueListenableBuilder<T> extends StatefulWidget {
|
||||
/// Creates a [ValueListenableBuilder].
|
||||
///
|
||||
/// The [valueListenable] and [builder] arguments must not be null.
|
||||
/// The [child] is optional but is good practice to use if part of the widget
|
||||
/// subtree does not depend on the value of the [valueListenable].
|
||||
const ValueListenableBuilder({
|
||||
@required this.valueListenable,
|
||||
@required this.builder,
|
||||
this.child,
|
||||
}) : assert(valueListenable != null),
|
||||
assert(builder != null);
|
||||
|
||||
/// The [ValueListenable] whose value you depend on in order to build.
|
||||
///
|
||||
/// This widget does not ensure that the [ValueListenable]'s value is not
|
||||
/// null, therefore your [builder] may need to handle null values.
|
||||
///
|
||||
/// This [ValueListenable] itself must not be null.
|
||||
final ValueListenable<T> valueListenable;
|
||||
|
||||
/// A [ValueWidgetBuilder] which builds a widget depending on the
|
||||
/// [valueListenable]'s value.
|
||||
///
|
||||
/// Can incorporate a [valueListenable] value-independent widget subtree
|
||||
/// from the [child] parameter into the returned widget tree.
|
||||
///
|
||||
/// Must not be null.
|
||||
final ValueWidgetBuilder<T> builder;
|
||||
|
||||
/// A [valueListenable]-independent widget which is passed back to the [builder].
|
||||
///
|
||||
/// This argument is optional and can be null if the entire widget subtree
|
||||
/// the [builder] builds depends on the value of the [valueListenable]. For
|
||||
/// example, if the [valueListenable] is a [String] and the [builder] simply
|
||||
/// returns a [Text] widget with the [String] value.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => new _ValueListenableBuilderState<T>();
|
||||
}
|
||||
|
||||
class _ValueListenableBuilderState<T> extends State<ValueListenableBuilder<T>> {
|
||||
T value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
value = widget.valueListenable.value;
|
||||
widget.valueListenable.addListener(_valueChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ValueListenableBuilder<T> oldWidget) {
|
||||
if (oldWidget.valueListenable != widget.valueListenable) {
|
||||
oldWidget.valueListenable.removeListener(_valueChanged);
|
||||
value = widget.valueListenable.value;
|
||||
widget.valueListenable.addListener(_valueChanged);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.valueListenable.removeListener(_valueChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _valueChanged() {
|
||||
setState(() { value = widget.valueListenable.value; });
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, value, widget.child);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ export 'src/widgets/ticker_provider.dart';
|
||||
export 'src/widgets/title.dart';
|
||||
export 'src/widgets/transitions.dart';
|
||||
export 'src/widgets/unique_widget.dart';
|
||||
export 'src/widgets/value_listenable_builder.dart';
|
||||
export 'src/widgets/viewport.dart';
|
||||
export 'src/widgets/visibility.dart';
|
||||
export 'src/widgets/widget_inspector.dart';
|
||||
|
||||
120
packages/flutter/test/widgets/value_listenable_builder_test.dart
Normal file
120
packages/flutter/test/widgets/value_listenable_builder_test.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2018 The Chromium 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_test/flutter_test.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
SpyStringValueNotifier valueListenable;
|
||||
Widget textBuilderUnderTest;
|
||||
|
||||
Widget builderForValueListenable(
|
||||
ValueListenable<String> valueListenable,
|
||||
) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ValueListenableBuilder<String>(
|
||||
valueListenable: valueListenable,
|
||||
builder: (BuildContext context, String value, Widget child) {
|
||||
if (value == null)
|
||||
return const Placeholder();
|
||||
return new Text(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
valueListenable = new SpyStringValueNotifier(null);
|
||||
textBuilderUnderTest = builderForValueListenable(valueListenable);
|
||||
});
|
||||
|
||||
testWidgets('Null value is ok', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(textBuilderUnderTest);
|
||||
|
||||
expect(find.byType(Placeholder), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Widget builds with initial value', (WidgetTester tester) async {
|
||||
valueListenable = new SpyStringValueNotifier('Bachman');
|
||||
|
||||
await tester.pumpWidget(builderForValueListenable(valueListenable));
|
||||
|
||||
expect(find.text('Bachman'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Widget updates when value changes', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(textBuilderUnderTest);
|
||||
|
||||
valueListenable.value = 'Gilfoyle';
|
||||
await tester.pump();
|
||||
expect(find.text('Gilfoyle'), findsOneWidget);
|
||||
|
||||
valueListenable.value = 'Dinesh';
|
||||
await tester.pump();
|
||||
expect(find.text('Gilfoyle'), findsNothing);
|
||||
expect(find.text('Dinesh'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Can change listenable', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(textBuilderUnderTest);
|
||||
|
||||
valueListenable.value = 'Gilfoyle';
|
||||
await tester.pump();
|
||||
expect(find.text('Gilfoyle'), findsOneWidget);
|
||||
|
||||
final ValueListenable<String> differentListenable =
|
||||
new SpyStringValueNotifier('Hendricks');
|
||||
|
||||
await tester.pumpWidget(builderForValueListenable(differentListenable));
|
||||
|
||||
expect(find.text('Gilfoyle'), findsNothing);
|
||||
expect(find.text('Hendricks'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Stops listening to old listenable after chainging listenable', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(textBuilderUnderTest);
|
||||
|
||||
valueListenable.value = 'Gilfoyle';
|
||||
await tester.pump();
|
||||
expect(find.text('Gilfoyle'), findsOneWidget);
|
||||
|
||||
final ValueListenable<String> differentListenable =
|
||||
new SpyStringValueNotifier('Hendricks');
|
||||
|
||||
await tester.pumpWidget(builderForValueListenable(differentListenable));
|
||||
|
||||
expect(find.text('Gilfoyle'), findsNothing);
|
||||
expect(find.text('Hendricks'), findsOneWidget);
|
||||
|
||||
// Change value of the (now) disconnected listenable.
|
||||
valueListenable.value = 'Big Head';
|
||||
|
||||
expect(find.text('Gilfoyle'), findsNothing);
|
||||
expect(find.text('Big Head'), findsNothing);
|
||||
expect(find.text('Hendricks'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Self-cleans when removed', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(textBuilderUnderTest);
|
||||
|
||||
valueListenable.value = 'Gilfoyle';
|
||||
await tester.pump();
|
||||
expect(find.text('Gilfoyle'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(const Placeholder());
|
||||
|
||||
expect(find.text('Gilfoyle'), findsNothing);
|
||||
expect(valueListenable.hasListeners, false);
|
||||
});
|
||||
}
|
||||
|
||||
class SpyStringValueNotifier extends ValueNotifier<String> {
|
||||
SpyStringValueNotifier(String initialValue) : super(initialValue);
|
||||
|
||||
/// Override for test visibility only.
|
||||
@override
|
||||
bool get hasListeners => super.hasListeners;
|
||||
}
|
||||
Reference in New Issue
Block a user