inherited_model.dart 7.91 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 26 27
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection';

import 'package:flutter/foundation.dart';

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.
///
28 29
/// {@youtube 560 315 https://www.youtube.com/watch?v=ml5uefGgkaA}
///
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/// 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
51
/// should be rebuilt. If [updateShouldNotify] returns true, then the
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 81 82
/// 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
83
/// `dependOnInheritedWidgetOfExactType`. They're represented as a [Set] because
84 85 86 87
/// 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.
88 89 90 91 92 93 94 95
///
/// 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.
96 97 98 99 100 101 102
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.
  const InheritedModel({ Key key, Widget child }) : super(key: key, child: child);

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

  /// 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].
122
  static void _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect, List<InheritedElement> results) {
123
    final InheritedElement model = context.getElementForInheritedWidgetOfExactType<T>();
124 125 126
    if (model == null)
      return;

127
    results.add(model);
128 129

    assert(model.widget is T);
130
    final T modelWidget = model.widget as T;
131 132 133 134 135 136 137 138 139 140 141
    if (modelWidget.isSupportedAspect(aspect))
      return;

    Element modelParent;
    model.visitAncestorElements((Element ancestor) {
      modelParent = ancestor;
      return false;
    });
    if (modelParent == null)
      return;

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

  /// 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
157
  /// `context.dependOnInheritedWidgetOfExactType<T>()`.
xster's avatar
xster committed
158 159
  ///
  /// If no ancestor of type T exists, null is returned.
160 161
  static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
    if (aspect == null)
162
      return context.dependOnInheritedWidgetOfExactType<T>();
163 164 165

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

172
    final InheritedElement lastModel = models.last;
173
    for (final InheritedElement model in models) {
174
      final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
      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
190
  InheritedModel<T> get widget => super.widget as InheritedModel<T>;
191 192 193

  @override
  void updateDependencies(Element dependent, Object aspect) {
194
    final Set<T> dependencies = getDependencies(dependent) as Set<T>;
195 196 197 198
    if (dependencies != null && dependencies.isEmpty)
      return;

    if (aspect == null) {
199
      setDependencies(dependent, HashSet<T>());
200 201
    } else {
      assert(aspect is T);
202
      setDependencies(dependent, (dependencies ?? HashSet<T>())..add(aspect as T));
203 204 205 206 207
    }
  }

  @override
  void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
208
    final Set<T> dependencies = getDependencies(dependent) as Set<T>;
209 210 211 212 213 214
    if (dependencies == null)
      return;
    if (dependencies.isEmpty || widget.updateShouldNotifyDependent(oldWidget, dependencies))
      dependent.didChangeDependencies();
  }
}