scroll-to-index
scroll-to-index copied to clipboard
controller.scrollToIndex stops working if list item rebuilds
If a child of AutoScrollTag rebuilds, then controller.scrollToIndex
does not cause the list to scroll.
See minimum working example: (uncomment below TODO to break scrolling behaviour)
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
// dummy data for the app
class CustomListItem {
final int index;
bool selected;
CustomListItem({
required this.index,
required this.selected,
});
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
// padding must exit faster bottom sheet otherwise it shows
final paddingDuration = const Duration(milliseconds: 200);
final scrollController = AutoScrollController();
MyHomePage({Key? key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double _bottomSheetHeight = 0;
List<CustomListItem> _listItems = List<CustomListItem>.generate(
20, (index) => CustomListItem(index: index, selected: false));
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedPadding(
duration: widget.paddingDuration,
padding: EdgeInsets.only(bottom: _bottomSheetHeight),
child: _buildBody(context)),
);
}
Widget _buildBody(BuildContext context) {
return ListView.builder(
controller: widget.scrollController,
itemCount: _listItems.length,
itemBuilder: (context, index) {
return AutoScrollTag(
key: ValueKey(index),
controller: widget.scrollController,
index: index,
child: _buildItem(context, index));
});
}
Widget _buildItem(BuildContext context, int index) {
final item = _listItems[index];
return Card(
color: index % 2 == 0 ? Colors.red : Colors.blue,
child: ListTile(
// TODO: uncommenting this line causes bug
// title: Text('Item ${item.index} (selected=${item.selected})'),
onTap: () {
_tappedItemAsync(context, index);
},
));
}
Future<void> _tappedItemAsync(BuildContext context, int index) async {
setState(() {
_listItems[index].selected = true;
});
// start showing the bottom sheet
final bottomSheetKey = GlobalKey();
final bottomSheetClosedFuture = showModalBottomSheet(
context: context,
builder: (context) {
return BottomSheet(
key: bottomSheetKey,
enableDrag: false,
onClosing: () {},
builder: (c) {
return const SizedBox(
height: 400,
);
});
});
// when the bottom starts closing reset the body padding
bottomSheetClosedFuture.then((value) {
setState(() {
_bottomSheetHeight = 0;
_listItems[index].selected = false;
});
});
// wait for the bottom sheet to have height
// then update the body padding
WidgetsBinding.instance?.addPostFrameCallback((_) {
final bottomSheetHeight = bottomSheetKey.currentContext?.size?.height;
if (bottomSheetHeight == null) {
throw Exception('bottomsheet has no height');
}
if (_bottomSheetHeight != bottomSheetHeight) {
setState(() {
_bottomSheetHeight = bottomSheetHeight;
});
}
});
// wait for the body padding to finish animating
await Future.delayed(
const Duration(milliseconds: 85) + widget.paddingDuration);
// scroll the item into bottom of newly constrained viewport
await widget.scrollController.scrollToIndex(index,
preferPosition: AutoScrollPosition.end,
duration: widget.paddingDuration);
}
}
@opringle hello, this is because you are not declaring the scrollController
in the state, so in each build a new scrollController
is created.
So declare de scrollController
in the state like this
...
class _MyHomePageState extends State<MyHomePage> {
final scrollController = AutoScrollController();
...
Adjusting the example in the way you suggests seems to solve the problem for me.