inherited_model.dart 7.83 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection';

import 'framework.dart';

/// An [InheritedWidget] that's intended to be used as the base class for
/// models whose dependents may only depend on one part or "aspect" of the
/// overall model.
///
/// An inherited widget's dependents are unconditionally rebuilt when the
/// inherited widget changes per [InheritedWidget.updateShouldNotify].
/// This widget is similar except that dependents aren't rebuilt
/// unconditionally.
///
/// Widgets that depend on an [InheritedModel] qualify their dependence
/// with a value that indicates what "aspect" of the model they depend
/// on. When the model is rebuilt, dependents will also be rebuilt, but
/// only if there was a change in the model that corresponds to the aspect
/// they provided.
///
/// The type parameter `T` is the type of the model aspect objects.
///
26 27
/// {@youtube 560 315 https://www.youtube.com/watch?v=ml5uefGgkaA}
///
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
/// Widgets create a dependency on an [InheritedModel] with a static method:
/// [InheritedModel.inheritFrom]. This method's `context` parameter
/// defines the subtree that will be rebuilt when the model changes.
/// Typically the `inheritFrom` method is called from a model-specific
/// static `of` method. For example:
///
/// ```dart
/// class MyModel extends InheritedModel<String> {
///   // ...
///   static MyModel of(BuildContext context, String aspect) {
///     return InheritedModel.inheritFrom<MyModel>(context, aspect: aspect);
///   }
/// }
/// ```
///
/// Calling `MyModel.of(context, 'foo')` means that `context` should only
/// be rebuilt when the `foo` aspect of `MyModel` changes. If the aspect
/// is null, then the model supports all aspects.
///
/// When the inherited model is rebuilt the [updateShouldNotify] and
/// [updateShouldNotifyDependent] methods are used to decide what
xster's avatar
xster committed
49
/// should be rebuilt. If [updateShouldNotify] returns true, then the
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
/// inherited model's [updateShouldNotifyDependent] method is tested for
/// each dependent and the set of aspect objects it depends on.
/// The [updateShouldNotifyDependent] method must compare the set of aspect
/// dependencies with the changes in the model itself.
///
/// For example:
///
/// ```dart
/// class ABModel extends InheritedModel<String> {
///   ABModel({ this.a, this.b, Widget child }) : super(child: child);
///
///   final int a;
///   final int b;
///
///   @override
///   bool updateShouldNotify(ABModel old) {
///     return a != old.a || b != old.b;
///   }
///
///   @override
///   bool updateShouldNotifyDependent(ABModel old, Set<String> aspects) {
///     return (a != old.a && aspects.contains('a'))
///         || (b != old.b && aspects.contains('b'))
///   }
///
///   // ...
/// }
/// ```
///
/// In the previous example the dependencies checked by
/// [updateShouldNotifyDependent] are just the aspect strings passed to
81
/// `dependOnInheritedWidgetOfExactType`. They're represented as a [Set] because
82 83 84 85
/// one Widget can depend on more than one aspect of the model.
/// 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.
86 87 88 89 90 91 92 93
///
/// 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.
94 95 96 97
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
  /// only be rebuilt if a specific aspect of the model changes.
98
  const InheritedModel({ Key? key, required Widget child }) : super(key: key, child: child);
99 100

  @override
101
  InheritedModelElement<T> createElement() => InheritedModelElement<T>(this);
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119

  /// Return true if the changes between this model and [oldWidget] match any
  /// of the [dependencies].
  @protected
  bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);

  /// Returns true if this model supports the given [aspect].
  ///
  /// Returns true by default: this model supports all aspects.
  ///
  /// Subclasses may override this method to indicate that they do not support
  /// all model aspects. This is typically done when a model can be used
  /// to "shadow" some aspects of an ancestor.
  @protected
  bool isSupportedAspect(Object aspect) => true;

  // The [result] will be a list of all of context's type T ancestors concluding
  // with the one that supports the specified model [aspect].
120
  static void _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect, List<InheritedElement> results) {
121
    final InheritedElement? model = context.getElementForInheritedWidgetOfExactType<T>();
122 123 124
    if (model == null)
      return;

125
    results.add(model);
126 127

    assert(model.widget is T);
128
    final T modelWidget = model.widget as T;
129 130 131
    if (modelWidget.isSupportedAspect(aspect))
      return;

132
    Element? modelParent;
133 134 135 136 137 138 139
    model.visitAncestorElements((Element ancestor) {
      modelParent = ancestor;
      return false;
    });
    if (modelParent == null)
      return;

140
    _findModels<T>(modelParent!, aspect, results);
141 142 143 144 145 146 147 148 149 150 151 152 153 154
  }

  /// Makes [context] dependent on the specified [aspect] of an [InheritedModel]
  /// of type T.
  ///
  /// When the given [aspect] of the model changes, the [context] will be
  /// rebuilt. The [updateShouldNotifyDependent] method must determine if a
  /// change in the model widget corresponds to an [aspect] value.
  ///
  /// The dependencies created by this method target all [InheritedModel] ancestors
  /// of type T up to and including the first one for which [isSupportedAspect]
  /// returns true.
  ///
  /// If [aspect] is null this method is the same as
155
  /// `context.dependOnInheritedWidgetOfExactType<T>()`.
xster's avatar
xster committed
156 157
  ///
  /// If no ancestor of type T exists, null is returned.
158
  static T? inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object? aspect }) {
159
    if (aspect == null)
160
      return context.dependOnInheritedWidgetOfExactType<T>();
161 162 163

    // Create a dependency on all of the type T ancestor models up until
    // a model is found for which isSupportedAspect(aspect) is true.
164 165
    final List<InheritedElement> models = <InheritedElement>[];
    _findModels<T>(context, aspect, models);
xster's avatar
xster committed
166 167 168 169
    if (models.isEmpty) {
      return null;
    }

170
    final InheritedElement lastModel = models.last;
171
    for (final InheritedElement model in models) {
172
      final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
      if (model == lastModel)
        return value;
    }

    assert(false);
    return null;
  }
}

/// An [Element] that uses a [InheritedModel] as its configuration.
class InheritedModelElement<T> extends InheritedElement {
  /// Creates an element that uses the given widget as its configuration.
  InheritedModelElement(InheritedModel<T> widget) : super(widget);

  @override
188 189
  void updateDependencies(Element dependent, Object? aspect) {
    final Set<T>? dependencies = getDependencies(dependent) as Set<T>?;
190 191 192 193
    if (dependencies != null && dependencies.isEmpty)
      return;

    if (aspect == null) {
194
      setDependencies(dependent, HashSet<T>());
195 196
    } else {
      assert(aspect is T);
197
      setDependencies(dependent, (dependencies ?? HashSet<T>())..add(aspect as T));
198 199 200 201 202
    }
  }

  @override
  void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
203
    final Set<T>? dependencies = getDependencies(dependent) as Set<T>?;
204 205
    if (dependencies == null)
      return;
206
    if (dependencies.isEmpty || (widget as InheritedModel<T>).updateShouldNotifyDependent(oldWidget, dependencies))
207 208 209
      dependent.didChangeDependencies();
  }
}