forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a ValueListenableBuilder (flutter#19729)
- Loading branch information
Showing
3 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
126 changes: 126 additions & 0 deletions
126
packages/flutter/lib/src/widgets/value_listenable_builder.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
packages/flutter/test/widgets/value_listenable_builder_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |