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 'package:flutter/foundation.dart';
8
9import 'framework.dart';
10import 'notification_listener.dart';
11import 'scroll_notification.dart';
12import 'scroll_position.dart';
13
14// Examples can assume:
15// void _listener(ScrollNotification notification) { }
16// late BuildContext context;
17
18/// A [ScrollNotification] listener for [ScrollNotificationObserver].
19///
20/// [ScrollNotificationObserver] is similar to
21/// [NotificationListener]. It supports a listener list instead of
22/// just a single listener and its listeners run unconditionally, they
23/// do not require a gating boolean return value.
24typedef ScrollNotificationCallback = void Function(ScrollNotification notification);
25
26class _ScrollNotificationObserverScope extends InheritedWidget {
27 const _ScrollNotificationObserverScope({
28 required super.child,
29 required ScrollNotificationObserverState scrollNotificationObserverState,
30 }) : _scrollNotificationObserverState = scrollNotificationObserverState;
31
32 final ScrollNotificationObserverState _scrollNotificationObserverState;
33
34 @override
35 bool updateShouldNotify(_ScrollNotificationObserverScope old) => _scrollNotificationObserverState != old._scrollNotificationObserverState;
36}
37
38final class _ListenerEntry extends LinkedListEntry<_ListenerEntry> {
39 _ListenerEntry(this.listener);
40 final ScrollNotificationCallback listener;
41}
42
43/// Notifies its listeners when a descendant scrolls.
44///
45/// To add a listener to a [ScrollNotificationObserver] ancestor:
46///
47/// ```dart
48/// ScrollNotificationObserver.of(context).addListener(_listener);
49/// ```
50///
51/// To remove the listener from a [ScrollNotificationObserver] ancestor:
52///
53/// ```dart
54/// ScrollNotificationObserver.of(context).removeListener(_listener);
55/// ```
56///
57/// Stateful widgets that share an ancestor [ScrollNotificationObserver] typically
58/// add a listener in [State.didChangeDependencies] (removing the old one
59/// if necessary) and remove the listener in their [State.dispose] method.
60///
61/// Any function with the [ScrollNotificationCallback] signature can act as a
62/// listener:
63///
64/// ```dart
65/// // (e.g. in a stateful widget)
66/// void _listener(ScrollNotification notification) {
67/// // Do something, maybe setState()
68/// }
69/// ```
70///
71/// This widget is similar to [NotificationListener]. It supports a listener
72/// list instead of just a single listener and its listeners run
73/// unconditionally, they do not require a gating boolean return value.
74///
75/// {@tool dartpad}
76/// This sample shows a "Scroll to top" button that uses [ScrollNotificationObserver]
77/// to listen for scroll notifications from [ListView]. The button is only visible
78/// when the user has scrolled down. When pressed, the button animates the scroll
79/// position of the [ListView] back to the top.
80///
81/// ** See code in examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart **
82/// {@end-tool}
83class ScrollNotificationObserver extends StatefulWidget {
84 /// Create a [ScrollNotificationObserver].
85 const ScrollNotificationObserver({
86 super.key,
87 required this.child,
88 });
89
90 /// The subtree below this widget.
91 final Widget child;
92
93 /// The closest instance of this class that encloses the given context.
94 ///
95 /// If there is no enclosing [ScrollNotificationObserver] widget, then null is
96 /// returned.
97 ///
98 /// Calling this method will create a dependency on the closest
99 /// [ScrollNotificationObserver] in the [context], if there is one.
100 ///
101 /// See also:
102 ///
103 /// * [ScrollNotificationObserver.of], which is similar to this method, but
104 /// asserts if no [ScrollNotificationObserver] ancestor is found.
105 static ScrollNotificationObserverState? maybeOf(BuildContext context) {
106 return context.dependOnInheritedWidgetOfExactType<_ScrollNotificationObserverScope>()?._scrollNotificationObserverState;
107 }
108
109 /// The closest instance of this class that encloses the given context.
110 ///
111 /// If no ancestor is found, this method will assert in debug mode, and throw
112 /// an exception in release mode.
113 ///
114 /// Calling this method will create a dependency on the closest
115 /// [ScrollNotificationObserver] in the [context].
116 ///
117 /// See also:
118 ///
119 /// * [ScrollNotificationObserver.maybeOf], which is similar to this method,
120 /// but returns null if no [ScrollNotificationObserver] ancestor is found.
121 static ScrollNotificationObserverState of(BuildContext context) {
122 final ScrollNotificationObserverState? observerState = maybeOf(context);
123 assert(() {
124 if (observerState == null) {
125 throw FlutterError(
126 'ScrollNotificationObserver.of() was called with a context that does not contain a '
127 'ScrollNotificationObserver widget.\n'
128 'No ScrollNotificationObserver widget ancestor could be found starting from the '
129 'context that was passed to ScrollNotificationObserver.of(). This can happen '
130 'because you are using a widget that looks for a ScrollNotificationObserver '
131 'ancestor, but no such ancestor exists.\n'
132 'The context used was:\n'
133 ' $context',
134 );
135 }
136 return true;
137 }());
138 return observerState!;
139 }
140
141 @override
142 ScrollNotificationObserverState createState() => ScrollNotificationObserverState();
143}
144
145/// The listener list state for a [ScrollNotificationObserver] returned by
146/// [ScrollNotificationObserver.of].
147///
148/// [ScrollNotificationObserver] is similar to
149/// [NotificationListener]. It supports a listener list instead of
150/// just a single listener and its listeners run unconditionally, they
151/// do not require a gating boolean return value.
152class ScrollNotificationObserverState extends State<ScrollNotificationObserver> {
153 LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>();
154
155 bool _debugAssertNotDisposed() {
156 assert(() {
157 if (_listeners == null) {
158 throw FlutterError(
159 'A $runtimeType was used after being disposed.\n'
160 'Once you have called dispose() on a $runtimeType, it can no longer be used.',
161 );
162 }
163 return true;
164 }());
165 return true;
166 }
167
168 /// Add a [ScrollNotificationCallback] that will be called each time
169 /// a descendant scrolls.
170 void addListener(ScrollNotificationCallback listener) {
171 assert(_debugAssertNotDisposed());
172 _listeners!.add(_ListenerEntry(listener));
173 }
174
175 /// Remove the specified [ScrollNotificationCallback].
176 void removeListener(ScrollNotificationCallback listener) {
177 assert(_debugAssertNotDisposed());
178 for (final _ListenerEntry entry in _listeners!) {
179 if (entry.listener == listener) {
180 entry.unlink();
181 return;
182 }
183 }
184 }
185
186 void _notifyListeners(ScrollNotification notification) {
187 assert(_debugAssertNotDisposed());
188 if (_listeners!.isEmpty) {
189 return;
190 }
191
192 final List<_ListenerEntry> localListeners = List<_ListenerEntry>.of(_listeners!);
193 for (final _ListenerEntry entry in localListeners) {
194 try {
195 if (entry.list != null) {
196 entry.listener(notification);
197 }
198 } catch (exception, stack) {
199 FlutterError.reportError(FlutterErrorDetails(
200 exception: exception,
201 stack: stack,
202 library: 'widget library',
203 context: ErrorDescription('while dispatching notifications for $runtimeType'),
204 informationCollector: () => <DiagnosticsNode>[
205 DiagnosticsProperty<ScrollNotificationObserverState>(
206 'The $runtimeType sending notification was',
207 this,
208 style: DiagnosticsTreeStyle.errorProperty,
209 ),
210 ],
211 ));
212 }
213 }
214 }
215
216 @override
217 Widget build(BuildContext context) {
218 return NotificationListener<ScrollMetricsNotification>(
219 onNotification: (ScrollMetricsNotification notification) {
220 // A ScrollMetricsNotification allows listeners to be notified for an
221 // initial state, as well as if the content dimensions change without
222 // scrolling.
223 _notifyListeners(notification.asScrollUpdate());
224 return false;
225 },
226 child: NotificationListener<ScrollNotification>(
227 onNotification: (ScrollNotification notification) {
228 _notifyListeners(notification);
229 return false;
230 },
231 child: _ScrollNotificationObserverScope(
232 scrollNotificationObserverState: this,
233 child: widget.child,
234 ),
235 ),
236 );
237 }
238
239 @override
240 void dispose() {
241 assert(_debugAssertNotDisposed());
242 _listeners = null;
243 super.dispose();
244 }
245}
246