mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This PR does a couple of things! https://user-images.githubusercontent.com/16964204/231897483-416287f9-50ce-468d-a714-2a4bc0f2e011.mov  Fixes #20819 Fixes #41910 Fixes #121419 ### Adds ScrollController.onAttach and ScrollController.onDetach This resolves a long held pain point for developers. When using a scroll controller, there is not scroll position until the scrollable widget is built, and almost all methods of notification are only triggered when scrolling happens. Adding these two methods will help developers gain access to the scroll position when it is created. A common workaround for this was using a post frame callback to access controller.position after the first frame, but this is ripe for issues such as having multiple positions attached to the controller, or the scrollable no longer existing after that post frame callback. I think this can also be helpful for folks to debug cases when the scroll controller has multiple positions attached. In particular, this also resolves this commented case: https://github.com/flutter/flutter/issues/20819#issuecomment-417784218 The isScrollingNotifier is hard for developers to access. ### Docs & samples I was surprised we did not have samples on scroll notification or scroll controller, so I overhauled it and added a lot of docs on all the different ways to access scrolling information, when it is available and how they differ.
164 lines
5.2 KiB
Dart
164 lines
5.2 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Flutter code sample for [ScrollController] & [ScrollNotification].
|
|
|
|
void main() => runApp(const ScrollNotificationDemo());
|
|
|
|
class ScrollNotificationDemo extends StatefulWidget {
|
|
const ScrollNotificationDemo({super.key});
|
|
|
|
@override
|
|
State<ScrollNotificationDemo> createState() => _ScrollNotificationDemoState();
|
|
}
|
|
|
|
class _ScrollNotificationDemoState extends State<ScrollNotificationDemo> {
|
|
ScrollNotification? _lastNotification;
|
|
late final ScrollController _controller;
|
|
bool _useController = true;
|
|
|
|
// This method handles the notification from the ScrollController.
|
|
void _handleControllerNotification() {
|
|
print('Notified through the scroll controller.');
|
|
// Access the position directly through the controller for details on the
|
|
// scroll position.
|
|
}
|
|
|
|
// This method handles the notification from the NotificationListener.
|
|
bool _handleScrollNotification(ScrollNotification notification) {
|
|
print('Notified through scroll notification.');
|
|
// The position can still be accessed through the scroll controller, but
|
|
// the notification object provides more details about the activity that is
|
|
// occurring.
|
|
if (_lastNotification.runtimeType != notification.runtimeType) {
|
|
setState(() {
|
|
// Call set state to respond to a change in the scroll notification.
|
|
_lastNotification = notification;
|
|
});
|
|
}
|
|
|
|
// Returning false allows the notification to continue bubbling up to
|
|
// ancestor listeners. If we wanted the notification to stop bubbling,
|
|
// return true.
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_controller = ScrollController();
|
|
if (_useController) {
|
|
// When listening to scrolling via the ScrollController, call
|
|
// `addListener` on the controller.
|
|
_controller.addListener(_handleControllerNotification);
|
|
}
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// ListView.separated works very similarly to this example with
|
|
// CustomScrollView & SliverList.
|
|
Widget body = CustomScrollView(
|
|
// Provide the scroll controller to the scroll view.
|
|
controller: _controller,
|
|
slivers: <Widget>[
|
|
SliverList.separated(
|
|
itemCount: 50,
|
|
itemBuilder: (_,int index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8.0,
|
|
horizontal: 20.0,
|
|
),
|
|
child: Text('Item $index'),
|
|
);
|
|
},
|
|
separatorBuilder: (_, __) => const Divider(
|
|
indent: 20,
|
|
endIndent: 20,
|
|
thickness: 2,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
if (!_useController) {
|
|
// If we are not using a ScrollController to listen to scrolling,
|
|
// let's use a NotificationListener. Similar, but with a different
|
|
// handler that provides information on what scrolling is occurring.
|
|
body = NotificationListener<ScrollNotification>(
|
|
onNotification: _handleScrollNotification,
|
|
child: body,
|
|
);
|
|
}
|
|
|
|
return MaterialApp(
|
|
theme: ThemeData.from(
|
|
useMaterial3: true,
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
|
|
),
|
|
home: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Listening to a ScrollPosition'),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(70),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: <Widget>[
|
|
if (!_useController) Text('Last notification: ${_lastNotification.runtimeType}'),
|
|
if (!_useController) const SizedBox.square(dimension: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
const Text('with:'),
|
|
Radio<bool>(
|
|
value: true,
|
|
groupValue: _useController,
|
|
onChanged: _handleRadioChange,
|
|
),
|
|
const Text('ScrollController'),
|
|
Radio<bool>(
|
|
value: false,
|
|
groupValue: _useController,
|
|
onChanged: _handleRadioChange,
|
|
),
|
|
const Text('NotificationListener'),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
body: body,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleRadioChange(bool? value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
if (value != _useController) {
|
|
setState(() {
|
|
// Respond to a change in selected radio button, and add/remove the
|
|
// listener to the scroll controller.
|
|
_useController = value;
|
|
if (_useController) {
|
|
_controller.addListener(_handleControllerNotification);
|
|
} else {
|
|
_controller.removeListener(_handleControllerNotification);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.removeListener(_handleControllerNotification);
|
|
super.dispose();
|
|
}
|
|
}
|