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) => _scrollNotificationObserverState != old._scrollNotificationObserverState;
39}
40
41final class _ListenerEntry extends LinkedListEntry<_ListenerEntry> {
42 _ListenerEntry(this.listener);
43 final ScrollNotificationCallback listener;
44}
45
46/// Notifies its listeners when a descendant scrolls.
47///
48/// To add a listener to a [ScrollNotificationObserver] ancestor:
49///
50/// ```dart
51/// ScrollNotificationObserver.of(context).addListener(_listener);
52/// ```
53///
54/// To remove the listener from a [ScrollNotificationObserver] ancestor:
55///
56/// ```dart
57/// ScrollNotificationObserver.of(context).removeListener(_listener);
58/// ```
59///
60/// Stateful widgets that share an ancestor [ScrollNotificationObserver] typically
61/// add a listener in [State.didChangeDependencies] (removing the old one
62/// if necessary) and remove the listener in their [State.dispose] method.
63///
64/// Any function with the [ScrollNotificationCallback] signature can act as a
65/// listener:
66///
67/// ```dart
68/// // (e.g. in a stateful widget)
69/// void _listener(ScrollNotification notification) {
70/// // Do something, maybe setState()
71/// }
72/// ```
73///
74/// This widget is similar to [NotificationListener]. It supports a listener
75/// list instead of just a single listener and its listeners run
76/// unconditionally, they do not require a gating boolean return value.
77///
78/// {@tool dartpad}
79/// This sample shows a "Scroll to top" button that uses [ScrollNotificationObserver]
80/// to listen for scroll notifications from [ListView]. The button is only visible
81/// when the user has scrolled down. When pressed, the button animates the scroll
82/// position of the [ListView] back to the top.
83///
84/// ** See code in examples/api/lib/widgets/scroll_notification_observer/scroll_notification_observer.0.dart **
85/// {@end-tool}
86class ScrollNotificationObserver extends StatefulWidget {
87 /// Create a [ScrollNotificationObserver].
88 const ScrollNotificationObserver({
89 super.key,
90 required this.child,
91 });
92
93 /// The subtree below this widget.
94 final Widget child;
95
96 /// The closest instance of this class that encloses the given context.
97 ///
98 /// If there is no enclosing [ScrollNotificationObserver] widget, then null is
99 /// returned.
100 ///
101 /// Calling this method will create a dependency on the closest
102 /// [ScrollNotificationObserver] in the [context], if there is one.
103 ///
104 /// See also:
105 ///
106 /// * [ScrollNotificationObserver.of], which is similar to this method, but
107 /// asserts if no [ScrollNotificationObserver] ancestor is found.
108 static ScrollNotificationObserverState? maybeOf(BuildContext context) {
109 return context.dependOnInheritedWidgetOfExactType<_ScrollNotificationObserverScope>()?._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(FlutterErrorDetails(
203 exception: exception,
204 stack: stack,
205 library: 'widget library',
206 context: ErrorDescription('while dispatching notifications for $runtimeType'),
207 informationCollector: () => <DiagnosticsNode>[
208 DiagnosticsProperty<ScrollNotificationObserverState>(
209 'The $runtimeType sending notification was',
210 this,
211 style: DiagnosticsTreeStyle.errorProperty,
212 ),
213 ],
214 ));
215 }
216 }
217 }
218
219 @override
220 Widget build(BuildContext context) {
221 return NotificationListener<ScrollMetricsNotification>(
222 onNotification: (ScrollMetricsNotification notification) {
223 // A ScrollMetricsNotification allows listeners to be notified for an
224 // initial state, as well as if the content dimensions change without
225 // scrolling.
226 _notifyListeners(notification.asScrollUpdate());
227 return false;
228 },
229 child: NotificationListener<ScrollNotification>(
230 onNotification: (ScrollNotification notification) {
231 _notifyListeners(notification);
232 return false;
233 },
234 child: _ScrollNotificationObserverScope(
235 scrollNotificationObserverState: this,
236 child: widget.child,
237 ),
238 ),
239 );
240 }
241
242 @override
243 void dispose() {
244 assert(_debugAssertNotDisposed());
245 _listeners = null;
246 super.dispose();
247 }
248}
249