1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6
7import 'framework.dart';
8
9/// An [InheritedWidget] that's intended to be used as the base class for models
10/// whose dependents may only depend on one part or "aspect" of the overall
11/// model.
12///
13/// An inherited widget's dependents are unconditionally rebuilt when the
14/// inherited widget changes per [InheritedWidget.updateShouldNotify]. This
15/// widget is similar except that dependents aren't rebuilt unconditionally.
16///
17/// Widgets that depend on an [InheritedModel] qualify their dependence with a
18/// value that indicates what "aspect" of the model they depend on. When the
19/// model is rebuilt, dependents will also be rebuilt, but only if there was a
20/// change in the model that corresponds to the aspect they provided.
21///
22/// The type parameter `T` is the type of the model aspect objects.
23///
24/// {@youtube 560 315 https://www.youtube.com/watch?v=ml5uefGgkaA}
25///
26/// Widgets create a dependency on an [InheritedModel] with a static method:
27/// [InheritedModel.inheritFrom]. This method's `context` parameter defines the
28/// subtree that will be rebuilt when the model changes. Typically the
29/// `inheritFrom` method is called from a model-specific static `maybeOf` or
30/// `of` methods, a convention that is present in many Flutter framework classes
31/// which look things up. For example:
32///
33/// ```dart
34/// class MyModel extends InheritedModel<String> {
35/// const MyModel({super.key, required super.child});
36///
37/// // ...
38/// static MyModel? maybeOf(BuildContext context, [String? aspect]) {
39/// return InheritedModel.inheritFrom<MyModel>(context, aspect: aspect);
40/// }
41///
42/// // ...
43/// static MyModel of(BuildContext context, [String? aspect]) {
44/// final MyModel? result = maybeOf(context, aspect);
45/// assert(result != null, 'Unable to find an instance of MyModel...');
46/// return result!;
47/// }
48/// }
49/// ```
50///
51/// Calling `MyModel.of(context, 'foo')` or `MyModel.maybeOf(context,
52/// 'foo')` means that `context` should only be rebuilt when the `foo` aspect of
53/// `MyModel` changes. If the `aspect` is null, then the model supports all
54/// aspects.
55///
56/// {@tool snippet}
57/// When the inherited model is rebuilt the [updateShouldNotify] and
58/// [updateShouldNotifyDependent] methods are used to decide what should be
59/// rebuilt. If [updateShouldNotify] returns true, then the inherited model's
60/// [updateShouldNotifyDependent] method is tested for each dependent and the
61/// set of aspect objects it depends on. The [updateShouldNotifyDependent]
62/// method must compare the set of aspect dependencies with the changes in the
63/// model itself. For example:
64///
65/// ```dart
66/// class ABModel extends InheritedModel<String> {
67/// const ABModel({
68/// super.key,
69/// this.a,
70/// this.b,
71/// required super.child,
72/// });
73///
74/// final int? a;
75/// final int? b;
76///
77/// @override
78/// bool updateShouldNotify(ABModel oldWidget) {
79/// return a != oldWidget.a || b != oldWidget.b;
80/// }
81///
82/// @override
83/// bool updateShouldNotifyDependent(ABModel oldWidget, Set<String> dependencies) {
84/// return (a != oldWidget.a && dependencies.contains('a'))
85/// || (b != oldWidget.b && dependencies.contains('b'));
86/// }
87///
88/// // ...
89/// }
90/// ```
91/// {@end-tool}
92///
93/// In the previous example the dependencies checked by
94/// [updateShouldNotifyDependent] are just the aspect strings passed to
95/// `dependOnInheritedWidgetOfExactType`. They're represented as a [Set] because
96/// one Widget can depend on more than one aspect of the model. If a widget
97/// depends on the model but doesn't specify an aspect, then changes in the
98/// model will cause the widget to be rebuilt unconditionally.
99///
100/// {@tool dartpad}
101/// This example shows how to implement [InheritedModel] to rebuild a widget
102/// based on a qualified dependence. When tapped on the "Resize Logo" button
103/// only the logo widget is rebuilt while the background widget remains
104/// unaffected.
105///
106/// ** See code in examples/api/lib/widgets/inherited_model/inherited_model.0.dart **
107/// {@end-tool}
108///
109/// See also:
110///
111/// * [InheritedWidget], an inherited widget that only notifies dependents when
112/// its value is different.
113/// * [InheritedNotifier], an inherited widget whose value can be a
114/// [Listenable], and which will notify dependents whenever the value sends
115/// notifications.
116abstract class InheritedModel<T> extends InheritedWidget {
117 /// Creates an inherited widget that supports dependencies qualified by
118 /// "aspects", i.e. a descendant widget can indicate that it should
119 /// only be rebuilt if a specific aspect of the model changes.
120 const InheritedModel({ super.key, required super.child });
121
122 @override
123 InheritedModelElement<T> createElement() => InheritedModelElement<T>(this);
124
125 /// Return true if the changes between this model and [oldWidget] match any
126 /// of the [dependencies].
127 @protected
128 bool updateShouldNotifyDependent(covariant InheritedModel<T> oldWidget, Set<T> dependencies);
129
130 /// Returns true if this model supports the given [aspect].
131 ///
132 /// Returns true by default: this model supports all aspects.
133 ///
134 /// Subclasses may override this method to indicate that they do not support
135 /// all model aspects. This is typically done when a model can be used
136 /// to "shadow" some aspects of an ancestor.
137 @protected
138 bool isSupportedAspect(Object aspect) => true;
139
140 // The [result] will be a list of all of context's type T ancestors concluding
141 // with the one that supports the specified model [aspect].
142 static void _findModels<T extends InheritedModel<Object>>(BuildContext context, Object aspect, List<InheritedElement> results) {
143 final InheritedElement? model = context.getElementForInheritedWidgetOfExactType<T>();
144 if (model == null) {
145 return;
146 }
147
148 results.add(model);
149
150 assert(model.widget is T);
151 final T modelWidget = model.widget as T;
152 if (modelWidget.isSupportedAspect(aspect)) {
153 return;
154 }
155
156 Element? modelParent;
157 model.visitAncestorElements((Element ancestor) {
158 modelParent = ancestor;
159 return false;
160 });
161 if (modelParent == null) {
162 return;
163 }
164
165 _findModels<T>(modelParent!, aspect, results);
166 }
167
168 /// Makes [context] dependent on the specified [aspect] of an [InheritedModel]
169 /// of type T.
170 ///
171 /// When the given [aspect] of the model changes, the [context] will be
172 /// rebuilt. The [updateShouldNotifyDependent] method must determine if a
173 /// change in the model widget corresponds to an [aspect] value.
174 ///
175 /// The dependencies created by this method target all [InheritedModel] ancestors
176 /// of type T up to and including the first one for which [isSupportedAspect]
177 /// returns true.
178 ///
179 /// If [aspect] is null this method is the same as
180 /// `context.dependOnInheritedWidgetOfExactType<T>()`.
181 ///
182 /// If no ancestor of type T exists, null is returned.
183 static T? inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object? aspect }) {
184 if (aspect == null) {
185 return context.dependOnInheritedWidgetOfExactType<T>();
186 }
187
188 // Create a dependency on all of the type T ancestor models up until
189 // a model is found for which isSupportedAspect(aspect) is true.
190 final List<InheritedElement> models = <InheritedElement>[];
191 _findModels<T>(context, aspect, models);
192 if (models.isEmpty) {
193 return null;
194 }
195
196 final InheritedElement lastModel = models.last;
197 for (final InheritedElement model in models) {
198 final T value = context.dependOnInheritedElement(model, aspect: aspect) as T;
199 if (model == lastModel) {
200 return value;
201 }
202 }
203
204 assert(false);
205 return null;
206 }
207}
208
209/// An [Element] that uses a [InheritedModel] as its configuration.
210class InheritedModelElement<T> extends InheritedElement {
211 /// Creates an element that uses the given widget as its configuration.
212 InheritedModelElement(InheritedModel<T> super.widget);
213
214 @override
215 void updateDependencies(Element dependent, Object? aspect) {
216 final Set<T>? dependencies = getDependencies(dependent) as Set<T>?;
217 if (dependencies != null && dependencies.isEmpty) {
218 return;
219 }
220
221 if (aspect == null) {
222 setDependencies(dependent, HashSet<T>());
223 } else {
224 assert(aspect is T);
225 setDependencies(dependent, (dependencies ?? HashSet<T>())..add(aspect as T));
226 }
227 }
228
229 @override
230 void notifyDependent(InheritedModel<T> oldWidget, Element dependent) {
231 final Set<T>? dependencies = getDependencies(dependent) as Set<T>?;
232 if (dependencies == null) {
233 return;
234 }
235 if (dependencies.isEmpty || (widget as InheritedModel<T>).updateShouldNotifyDependent(oldWidget, dependencies)) {
236 dependent.didChangeDependencies();
237 }
238 }
239}
240