scroll-to-index icon indicating copy to clipboard operation
scroll-to-index copied to clipboard

controller.scrollToIndex stops working if list item rebuilds

Open opringle opened this issue 3 years ago • 2 comments

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 avatar Dec 11 '21 00:12 opringle

@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();
...

MiniSuperDev avatar May 13 '22 03:05 MiniSuperDev

Adjusting the example in the way you suggests seems to solve the problem for me.

abulka avatar Oct 19 '22 00:10 abulka