Create a ValueListenableBuilder (#19729)

diff --git a/packages/flutter/lib/src/widgets/value_listenable_builder.dart b/packages/flutter/lib/src/widgets/value_listenable_builder.dart
new file mode 100644
index 0000000..4ca7c7a
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/value_listenable_builder.dart
@@ -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);
+  }
+}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index 3ec5fe3..72920b0 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -98,6 +98,7 @@
 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';
diff --git a/packages/flutter/test/widgets/value_listenable_builder_test.dart b/packages/flutter/test/widgets/value_listenable_builder_test.dart
new file mode 100644
index 0000000..293d974e
--- /dev/null
+++ b/packages/flutter/test/widgets/value_listenable_builder_test.dart
@@ -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;
+}