Unverified Commit 93132859 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

[H] Created a variant of InheritedWidget specifically for Listenables (#23393)

* Created a variant of InheritedWidget specifically for Listenables

* Add more documentation per review comments
parent 5e7b0a36
......@@ -10,6 +10,49 @@ import 'diagnostics.dart';
import 'observer_list.dart';
/// An object that maintains a list of listeners.
///
/// The listeners are typically used to notify clients that the object has been
/// updated.
///
/// There are two variants of this interface:
///
/// * [ValueListenable], an interface that augments the [Listenable] interface
/// with the concept of a _current value_.
///
/// * [Animation], an interface that augments the [ValueListenable] interface
/// to add the concept of direction (forward or reverse).
///
/// Many classes in the Flutter API use or implement these interfaces. The
/// following subclasses are especially relevant:
///
/// * [ChangeNotifier], which can be subclassed or mixed in to create objects
/// that implement the [Listenable] interface.
///
/// * [ValueNotifier], which implements the [ValueListenable] interface with
/// a mutable value that triggers the notifications when modified.
///
/// The terms "notify clients", "send notifications", "trigger notifications",
/// and "fire notifications" are used interchangeably.
///
/// See also:
///
/// * [AnimatedBuilder], a widget that uses a builder callback to rebuild
/// whenever a given [Listenable] triggers its notifications. This widget is
/// commonly used with [Animation] subclasses, wherein its name. It is a
/// subclass of [AnimatedWidget], which can be used to create widgets that
/// are driven from a [Listenable].
///
/// * [ValueListenableBuilder], a widget that uses a builder callback to
/// rebuild whenever a [ValueListenable] object triggers its notifications,
/// providing the builder with the value of the object.
///
/// * [InheritedNotifier], an abstract superclass for widgets that use a
/// [Listenable]'s notifications to trigger rebuilds in descendant widgets
/// that declare a dependency on them, using the [InheritedWidget] mechanism.
///
/// * [new Listenable.merge], which creates a [Listenable] that triggers
/// notifications whenever any of a list of other [Listenable]s trigger their
/// notifications.
abstract class Listenable {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -37,6 +80,10 @@ abstract class Listenable {
/// This interface is implemented by [ValueNotifier<T>] and [Animation<T>], and
/// allows other APIs to accept either of those implementations interchangeably.
abstract class ValueListenable<T> extends Listenable {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ValueListenable();
/// The current value of the object. When the value changes, the callbacks
/// registered with [addListener] will be invoked.
T get value;
......
......@@ -1545,6 +1545,11 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
/// * [StatelessWidget], for widgets that always build the same way given a
/// particular configuration and ambient state.
/// * [Widget], for an overview of widgets in general.
/// * [InheritedNotifier], an inherited widget whose value can be a
/// [Listenable], and which will notify dependents whenever the value
/// sends notifications.
/// * [InheritedModel], an inherited widget that allows clients to subscribe
/// to changes for subparts of the value.
abstract class InheritedWidget extends ProxyWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -3946,15 +3951,26 @@ abstract class ProxyElement extends ComponentElement {
assert(widget != newWidget);
super.update(newWidget);
assert(widget == newWidget);
notifyClients(oldWidget);
updated(oldWidget);
_dirty = true;
rebuild();
}
/// Notify other objects that the widget associated with this element has changed.
/// Called during build when the [widget] has changed.
///
/// Called during [update] after changing the widget associated with this
/// element but before rebuilding this element.
/// By default, calls [notifyClients]. Subclasses may override this method to
/// avoid calling [notifyClients] unnecessarily (e.g. if the old and new
/// widgets are equivalent).
@protected
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}
/// Notify other objects that the widget associated with this element has
/// changed.
///
/// Called during [update] (via [updated]) after changing the widget
/// associated with this element but before rebuilding this element.
@protected
void notifyClients(covariant ProxyWidget oldWidget);
}
......@@ -4191,18 +4207,28 @@ class InheritedElement extends ProxyElement {
/// Calls [Element.didChangeDependencies] of all dependent elements, if
/// [InheritedWidget.updateShouldNotify] returns true.
///
/// Notifies all dependent elements that this inherited widget has changed.
/// Called by [update], immediately prior to [build].
///
/// [InheritedElement] calls this function if the [widget]'s
/// [InheritedWidget.updateShouldNotify] returns true.
/// Calls [notifyClients] to actually trigger the notifications.
@override
void updated(InheritedWidget oldWidget) {
if (widget.updateShouldNotify(oldWidget))
super.updated(oldWidget);
}
/// Notifies all dependent elements that this inherited widget has changed, by
/// calling [Element.didChangeDependencies].
///
/// This method must only be called during the build phase. Usually this
/// method is called automatically when an inherited widget is rebuilt, e.g.
/// as a result of calling [State.setState] above the inherited widget.
///
/// This method must be called during the build phase. Usually this method is
/// called automatically when an inherited widget is rebuilt, e.g. as a
/// result of calling [State.setState] above the inherited widget.
/// See also:
///
/// * [InheritedNotifier], a subclass of [InheritedWidget] that also calls
/// this method when its [Listenable] sends a notification.
@override
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents.keys) {
assert(() {
......
......@@ -83,6 +83,14 @@ import 'framework.dart';
/// If a widget depends on the model but doesn't specify an aspect,
/// then changes in the model will cause the widget to be rebuilt
/// unconditionally.
///
/// See also:
///
/// * [InheritedWidget], an inherited widget that only notifies dependents
/// when its value is different.
/// * [InheritedNotifier], an inherited widget whose value can be a
/// [Listenable], and which will notify dependents whenever the value
/// sends notifications.
abstract class InheritedModel<T> extends InheritedWidget {
/// Creates an inherited widget that supports dependencies qualified by
/// "aspects", i.e. a descendant widget can indicate that it should
......
// 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';
/// An inherited widget for a [Listenable] [notifier], which updates its
/// dependencies when the [notifier] is triggered.
///
/// This is a variant of [InheritedWidget], specialised for subclasses of
/// [Listenable], such as [ChangeNotifier] or [ValueNotifier].
///
/// Dependents are notified whenever the [notifier] sends notifications, or
/// whenever the identity of the [notifier] changes.
///
/// Multiple notifications are coalesced, so that dependents only rebuild once
/// even if the [notifier] fires multiple times between two frames.
///
/// Typically this class is subclassed with a class that provides an `of` static
/// method that calls [BuildContext.inheritFromWidgetOfExactType] with that
/// class.
///
/// The [updateShouldNotify] method may also be overridden, to change the logic
/// in the cases where [notifier] itself is changed. The [updateShouldNotify]
/// method is called with the old [notifier] in the case of the [notifier] being
/// changed. When it returns true, the dependents are marked as needing to be
/// rebuilt this frame.
///
/// See also:
///
/// * [Animation], an implementation of [Listenable] that ticks each frame to
/// update a value.
/// * [ViewportOffset] or its subclass [ScrollPosition], implementations of
/// [Listenable] that trigger when a view is scrolled.
/// * [InheritedWidget], an inherited widget that only notifies dependents
/// when its value is different.
/// * [InheritedModel], an inherited widget that allows clients to subscribe
/// to changes for subparts of the value.
abstract class InheritedNotifier<T extends Listenable> extends InheritedWidget {
/// Create an inherited widget that updates its dependents when [notifier]
/// sends notifications.
///
/// The [child] argument must not be null.
const InheritedNotifier({
Key key,
this.notifier,
@required Widget child,
}) : assert(child != null),
super(key: key, child: child);
/// The [Listenable] object to which to listen.
///
/// Whenever this object sends change notifications, the dependents of this
/// widget are triggered.
///
/// By default, whenever the [notifier] is changed (including when changing to
/// or from null), if the old notifier is not equal to the new notifier (as
/// determined by the `==` operator), notifications are sent. This behavior
/// can be overridden by overriding [updateShouldNotify].
///
/// While the [notifier] is null, no notifications are sent, since the null
/// object cannot itself send notifications.
final T notifier;
@override
bool updateShouldNotify(InheritedNotifier<T> oldWidget) {
return oldWidget.notifier != notifier;
}
@override
_InheritedNotifierElement<T> createElement() => _InheritedNotifierElement<T>(this);
}
class _InheritedNotifierElement<T extends Listenable> extends InheritedElement {
_InheritedNotifierElement(InheritedNotifier<T> widget) : super(widget) {
widget.notifier?.addListener(_handleUpdate);
}
@override
InheritedNotifier<T> get widget => super.widget;
bool _dirty = false;
@override
void update(InheritedNotifier<T> newWidget) {
final T oldNotifier = widget.notifier;
final T newNotifier = newWidget.notifier;
if (oldNotifier != newNotifier) {
oldNotifier?.removeListener(_handleUpdate);
newNotifier?.addListener(_handleUpdate);
}
super.update(newWidget);
}
@override
Widget build() {
if (_dirty)
notifyClients(widget);
return super.build();
}
void _handleUpdate() {
_dirty = true;
markNeedsBuild();
}
@override
void notifyClients(InheritedNotifier<T> oldWidget) {
super.notifyClients(oldWidget);
_dirty = false;
}
@override
void unmount() {
widget.notifier?.removeListener(_handleUpdate);
super.unmount();
}
}
......@@ -47,6 +47,7 @@ export 'src/widgets/image.dart';
export 'src/widgets/image_icon.dart';
export 'src/widgets/implicit_animations.dart';
export 'src/widgets/inherited_model.dart';
export 'src/widgets/inherited_notifier.dart';
export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart';
......
......@@ -52,6 +52,11 @@ class ExpectFailState extends State<ExpectFail> {
Widget build(BuildContext context) => Container();
}
class ChangeNotifierInherited extends InheritedNotifier<ChangeNotifier> {
const ChangeNotifierInherited({ Key key, Widget child, ChangeNotifier notifier })
: super(key: key, child: child, notifier: notifier);
}
void main() {
testWidgets('Inherited notifies dependents', (WidgetTester tester) async {
final List<TestInherited> log = <TestInherited>[];
......@@ -472,4 +477,43 @@ void main() {
expect(exceptionCaught, isTrue);
});
testWidgets('InheritedNotifier', (WidgetTester tester) async {
int buildCount = 0;
final ChangeNotifier notifier = ChangeNotifier();
final Widget builder = Builder(
builder: (BuildContext context) {
context.inheritFromWidgetOfExactType(ChangeNotifierInherited);
buildCount += 1;
return Container();
}
);
final Widget inner = ChangeNotifierInherited(
notifier: notifier,
child: builder,
);
await tester.pumpWidget(inner);
expect(buildCount, equals(1));
await tester.pumpWidget(inner);
expect(buildCount, equals(1));
await tester.pump();
expect(buildCount, equals(1));
notifier.notifyListeners(); // ignore: invalid_use_of_protected_member
await tester.pump();
expect(buildCount, equals(2));
await tester.pumpWidget(inner);
expect(buildCount, equals(2));
await tester.pumpWidget(ChangeNotifierInherited(
notifier: null,
child: builder,
));
expect(buildCount, equals(3));
});
}
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