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