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 'framework.dart';
6
7// Examples can assume:
8// class MyWidget extends StatelessWidget { const MyWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) => child; }
9
10/// A lookup boundary controls what entities are visible to descendants of the
11/// boundary via the static lookup methods provided by the boundary.
12///
13/// The static lookup methods of the boundary mirror the lookup methods by the
14/// same name exposed on [BuildContext] and they can be used as direct
15/// replacements. Unlike the methods on [BuildContext], these methods do not
16/// find any ancestor entities of the closest [LookupBoundary] surrounding the
17/// provided [BuildContext]. The root of the tree is an implicit lookup boundary.
18///
19/// {@tool snippet}
20/// In the example below, the [LookupBoundary.findAncestorWidgetOfExactType]
21/// call returns null because the [LookupBoundary] "hides" `MyWidget` from the
22/// [BuildContext] that was queried.
23///
24/// ```dart
25/// MyWidget(
26/// child: LookupBoundary(
27/// child: Builder(
28/// builder: (BuildContext context) {
29/// MyWidget? widget = LookupBoundary.findAncestorWidgetOfExactType<MyWidget>(context);
30/// return Text('$widget'); // "null"
31/// },
32/// ),
33/// ),
34/// )
35/// ```
36/// {@end-tool}
37///
38/// A [LookupBoundary] only affects the behavior of the static lookup methods
39/// defined on the boundary. It does not affect the behavior of the lookup
40/// methods defined on [BuildContext].
41///
42/// A [LookupBoundary] is rarely instantiated directly. They are inserted at
43/// locations of the widget tree where the render tree diverges from the element
44/// tree, which is rather uncommon. Such anomalies are created by
45/// [RenderObjectElement]s that don't attach their [RenderObject] to the closest
46/// ancestor [RenderObjectElement], e.g. because they bootstrap a separate
47/// stand-alone render tree.
48// TODO(goderbauer): Reference the View widget here once available.
49/// This behavior breaks the assumption some widgets have about the structure of
50/// the render tree: These widgets may try to reach out to an ancestor widget,
51/// assuming that their associated [RenderObject]s are also ancestors, which due
52/// to the anomaly may not be the case. At the point where the divergence in the
53/// two trees is introduced, a [LookupBoundary] can be used to hide that ancestor
54/// from the querying widget.
55///
56/// As an example, [Material.of] relies on lookup boundaries to hide the
57/// [Material] widget from certain descendant button widget. Buttons reach out
58/// to their [Material] ancestor to draw ink splashes on its associated render
59/// object. This only produces the desired effect if the button render object
60/// is a descendant of the [Material] render object. If the element tree and
61/// the render tree are not in sync due to anomalies described above, this may
62/// not be the case. To avoid incorrect visuals, the [Material] relies on
63/// lookup boundaries to hide itself from descendants in subtrees with such
64/// anomalies. Those subtrees are expected to introduce their own [Material]
65/// widget that buttons there can utilize without crossing a lookup boundary.
66class LookupBoundary extends InheritedWidget {
67 /// Creates a [LookupBoundary].
68 ///
69 /// A none-null [child] widget must be provided.
70 const LookupBoundary({super.key, required super.child});
71
72 /// Obtains the nearest widget of the given type `T` within the current
73 /// [LookupBoundary] of `context`, which must be the type of a concrete
74 /// [InheritedWidget] subclass, and registers the provided build `context`
75 /// with that widget such that when that widget changes (or a new widget of
76 /// that type is introduced, or the widget goes away), the build context is
77 /// rebuilt so that it can obtain new values from that widget.
78 ///
79 /// This method behaves exactly like
80 /// [BuildContext.dependOnInheritedWidgetOfExactType], except it only
81 /// considers [InheritedWidget]s of the specified type `T` between the
82 /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
83 /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
84 /// method. The root of the tree is treated as an implicit lookup boundary.
85 ///
86 /// {@macro flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType}
87 static T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context, { Object? aspect }) {
88 // The following call makes sure that context depends on something so
89 // Element.didChangeDependencies is called when context moves in the tree
90 // even when requested dependency remains unfulfilled (i.e. null is
91 // returned).
92 context.dependOnInheritedWidgetOfExactType<LookupBoundary>();
93 final InheritedElement? candidate = getElementForInheritedWidgetOfExactType<T>(context);
94 if (candidate == null) {
95 return null;
96 }
97 context.dependOnInheritedElement(candidate, aspect: aspect);
98 return candidate.widget as T;
99 }
100
101 /// Obtains the element corresponding to the nearest widget of the given type
102 /// `T` within the current [LookupBoundary] of `context`.
103 ///
104 /// `T` must be the type of a concrete [InheritedWidget] subclass. Returns
105 /// null if no such element is found.
106 ///
107 /// This method behaves exactly like
108 /// [BuildContext.getElementForInheritedWidgetOfExactType], except it only
109 /// considers [InheritedWidget]s of the specified type `T` between the
110 /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
111 /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
112 /// method. The root of the tree is treated as an implicit lookup boundary.
113 ///
114 /// {@macro flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType}
115 static InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context) {
116 final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType<T>();
117 if (candidate == null) {
118 return null;
119 }
120 final Element? boundary = context.getElementForInheritedWidgetOfExactType<LookupBoundary>();
121 if (boundary != null && boundary.depth > candidate.depth) {
122 return null;
123 }
124 return candidate;
125 }
126
127 /// Returns the nearest ancestor widget of the given type `T` within the
128 /// current [LookupBoundary] of `context`.
129 ///
130 /// `T` must be the type of a concrete [Widget] subclass.
131 ///
132 /// This method behaves exactly like
133 /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
134 /// [Widget]s of the specified type `T` between the provided [BuildContext]
135 /// and its closest [LookupBoundary] ancestor. [Widget]s past that
136 /// [LookupBoundary] are invisible to this method. The root of the tree is
137 /// treated as an implicit lookup boundary.
138 ///
139 /// {@macro flutter.widgets.BuildContext.findAncestorWidgetOfExactType}
140 static T? findAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
141 Element? target;
142 context.visitAncestorElements((Element ancestor) {
143 if (ancestor.widget.runtimeType == T) {
144 target = ancestor;
145 return false;
146 }
147 return ancestor.widget.runtimeType != LookupBoundary;
148 });
149 return target?.widget as T?;
150 }
151
152 /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
153 /// within the current [LookupBoundary] of `context` that is an instance of
154 /// the given type `T`.
155 ///
156 /// This method behaves exactly like
157 /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
158 /// [State] objects of the specified type `T` between the provided
159 /// [BuildContext] and its closest [LookupBoundary] ancestor. [State] objects
160 /// past that [LookupBoundary] are invisible to this method. The root of the
161 /// tree is treated as an implicit lookup boundary.
162 ///
163 /// {@macro flutter.widgets.BuildContext.findAncestorStateOfType}
164 static T? findAncestorStateOfType<T extends State>(BuildContext context) {
165 StatefulElement? target;
166 context.visitAncestorElements((Element ancestor) {
167 if (ancestor is StatefulElement && ancestor.state is T) {
168 target = ancestor;
169 return false;
170 }
171 return ancestor.widget.runtimeType != LookupBoundary;
172 });
173 return target?.state as T?;
174 }
175
176 /// Returns the [State] object of the furthest ancestor [StatefulWidget]
177 /// widget within the current [LookupBoundary] of `context` that is an
178 /// instance of the given type `T`.
179 ///
180 /// This method behaves exactly like
181 /// [BuildContext.findRootAncestorStateOfType], except it considers the
182 /// closest [LookupBoundary] ancestor of `context` to be the root. [State]
183 /// objects past that [LookupBoundary] are invisible to this method. The root
184 /// of the tree is treated as an implicit lookup boundary.
185 ///
186 /// {@macro flutter.widgets.BuildContext.findRootAncestorStateOfType}
187 static T? findRootAncestorStateOfType<T extends State>(BuildContext context) {
188 StatefulElement? target;
189 context.visitAncestorElements((Element ancestor) {
190 if (ancestor is StatefulElement && ancestor.state is T) {
191 target = ancestor;
192 }
193 return ancestor.widget.runtimeType != LookupBoundary;
194 });
195 return target?.state as T?;
196 }
197
198 /// Returns the [RenderObject] object of the nearest ancestor
199 /// [RenderObjectWidget] widget within the current [LookupBoundary] of
200 /// `context` that is an instance of the given type `T`.
201 ///
202 /// This method behaves exactly like
203 /// [BuildContext.findAncestorRenderObjectOfType], except it only considers
204 /// [RenderObject]s of the specified type `T` between the provided
205 /// [BuildContext] and its closest [LookupBoundary] ancestor. [RenderObject]s
206 /// past that [LookupBoundary] are invisible to this method. The root of the
207 /// tree is treated as an implicit lookup boundary.
208 ///
209 /// {@macro flutter.widgets.BuildContext.findAncestorRenderObjectOfType}
210 static T? findAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
211 Element? target;
212 context.visitAncestorElements((Element ancestor) {
213 if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
214 target = ancestor;
215 return false;
216 }
217 return ancestor.widget.runtimeType != LookupBoundary;
218 });
219 return target?.renderObject as T?;
220 }
221
222 /// Walks the ancestor chain, starting with the parent of the build context's
223 /// widget, invoking the argument for each ancestor until a [LookupBoundary]
224 /// or the root is reached.
225 ///
226 /// This method behaves exactly like [BuildContext.visitAncestorElements],
227 /// except it only walks the tree up to the closest [LookupBoundary] ancestor
228 /// of the provided context. The root of the tree is treated as an implicit
229 /// lookup boundary.
230 ///
231 /// {@macro flutter.widgets.BuildContext.visitAncestorElements}
232 static void visitAncestorElements(BuildContext context, ConditionalElementVisitor visitor) {
233 context.visitAncestorElements((Element ancestor) {
234 return visitor(ancestor) && ancestor.widget.runtimeType != LookupBoundary;
235 });
236 }
237
238 /// Walks the non-[LookupBoundary] child [Element]s of the provided
239 /// `context`.
240 ///
241 /// This method behaves exactly like [BuildContext.visitChildElements],
242 /// except it only visits children that are not a [LookupBoundary].
243 ///
244 /// {@macro flutter.widgets.BuildContext.visitChildElements}
245 static void visitChildElements(BuildContext context, ElementVisitor visitor) {
246 context.visitChildElements((Element child) {
247 if (child.widget.runtimeType != LookupBoundary) {
248 visitor(child);
249 }
250 });
251 }
252
253 /// Returns true if a [LookupBoundary] is hiding the nearest
254 /// [Widget] of the specified type `T` from the provided [BuildContext].
255 ///
256 /// This method throws when asserts are disabled.
257 static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
258 bool? result;
259 assert(() {
260 bool hiddenByBoundary = false;
261 bool ancestorFound = false;
262 context.visitAncestorElements((Element ancestor) {
263 if (ancestor.widget.runtimeType == T) {
264 ancestorFound = true;
265 return false;
266 }
267 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
268 return true;
269 });
270 result = ancestorFound & hiddenByBoundary;
271 return true;
272 } ());
273 return result!;
274 }
275
276 /// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget]
277 /// with a [State] of the specified type `T` from the provided [BuildContext].
278 ///
279 /// This method throws when asserts are disabled.
280 static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) {
281 bool? result;
282 assert(() {
283 bool hiddenByBoundary = false;
284 bool ancestorFound = false;
285 context.visitAncestorElements((Element ancestor) {
286 if (ancestor is StatefulElement && ancestor.state is T) {
287 ancestorFound = true;
288 return false;
289 }
290 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
291 return true;
292 });
293 result = ancestorFound & hiddenByBoundary;
294 return true;
295 } ());
296 return result!;
297 }
298
299 /// Returns true if a [LookupBoundary] is hiding the nearest
300 /// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
301 /// from the provided [BuildContext].
302 ///
303 /// This method throws when asserts are disabled.
304 static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
305 bool? result;
306 assert(() {
307 bool hiddenByBoundary = false;
308 bool ancestorFound = false;
309 context.visitAncestorElements((Element ancestor) {
310 if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
311 ancestorFound = true;
312 return false;
313 }
314 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
315 return true;
316 });
317 result = ancestorFound & hiddenByBoundary;
318 return true;
319 } ());
320 return result!;
321 }
322
323 @override
324 bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
325}
326