I'm doing something like a news feed and I'm using CustomScrollView with two slivers (one for initially loaded news and one for new news that should not shift elements when loaded).
Example:
return CustomScrollView(
center: centerKey,
slivers: [
renderOneList(newItems),
renderOneList(oldItems, centerKey),
],
);
Everything works as it should. I use UniqueKey to define the first element where the initial reading starts. Once every X seconds, I load new items and put them into an array of newItems. They are added to the top and the user can scroll to the top.
However, when clicking on the status bar in iOS Primary Scroll Controller scrolls to position 0 (which in my case is the boundary of new news and old news). This is correct from Flutter's point of view, but it's not the behavior I would like to achieve. Any ideas on how to scroll to the top?
What I think about it:
I ended up with a dead end, but I'm sure the community has solved such a basic problem before. Hope for some help.
This is possible using Scrollable.ensureVisible
.
I don't know how renderOneList
is implemented so let's assume it's a SliverList
on which you optionally set the key
(used for centerKey
).
If that's the case, then you can simply set a global key on the first sliver too and use this key with ensureVisible
.
Here's a working example:
import 'package:flutter/material.dart';
class ScrollToTopExample extends StatelessWidget {
const ScrollToTopExample({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Scaffold(
appBar: AppBar(
title: GestureDetector(
onTap: scrollToTop,
child: Text('Scroll to top example'),
),
),
body: CustomScrollView(
center: GlobalObjectKey('center-key'),
slivers: [
SliverList(
key: GlobalObjectKey('top-key'),
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item -${index + 1}'),
),
childCount: 100,
),
),
SliverList(
key: GlobalObjectKey('center-key'),
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('Item ${index + 1}'),
),
childCount: 100,
),
),
],
),
),
// Status bar tap override
Positioned(
top: 0,
left: 0,
right: 0,
height: MediaQuery.of(context).padding.top,
child: GestureDetector(
excludeFromSemantics: true,
onTap: onStatusBarTap,
),
),
],
);
}
void scrollToTop() {
final key = GlobalObjectKey('top-key');
final context = key.currentContext;
if (context == null) {
return;
}
Scrollable.ensureVisible(
context,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: 0.0,
alignmentPolicy: ScrollPositionAlignmentPolicy.explicit,
);
}
void onStatusBarTap() {
print('onStatusBarTap');
scrollToTop();
}
}
The important part here is the alignment: 0
parameter together with alignmentPolicy: ScrollPositionAlignmentPolicy.explicit
which ensures that the top of the target element is aligned with the top of the scrollable container.
If you need more fine-grained control of the scroll position (eg. scroll to a certain item) you will need to set a key on the item's widget and make sure the widget is in the tree. For this reason, I believe using Scrollable.ensureVisible
will not work to scroll to a certain item when the list is built dynamically using ListView.builder
, SliverList.builder
(or SliverChildBuilderDelegate
).