6

Im trying to build List where you can add items on both sides which are limited (so its not infinite on both sides). On top you would do refresh, and it will load new items but you would stay on same place (and new items are on top of your current position). On bottom you gonna load more but same, stay on same place. I have tried doing this using CustomScrollView and two slivers with center, but thing that i want to do is that on refresh i want to delete some old items thing what happens is that it jumps to new center and refreshes the cards. Which is it that point on top.

Current state

CustomScrollView(
                physics: const BouncingScrollPhysics(
                  parent: AlwaysScrollableScrollPhysics(),
                ),
                center: _mainListKey,
                controller: scrollController,
                cacheExtent: 1000,
                slivers: <Widget>[
                  SliverList(
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
                     return ComplexWidget(item: topItems[index]),
                  },
                  childCount: topItems.count,
                  ),),
                   SliverList(
                      key:  _mainListKey
                      delegate: SliverChildBuilderDelegate(
                        (BuildContext context, int index) {
                     return ComplexWidget(item: bottomItems[index]),
                  },
                  childCount: bottomItems.count,
                  ),),
                ]
              ),

Better description of behavior:

Well, imagine same list like here in twitter app. If you scroll down, it will load more and more, older and older, tweets. Sometimes it will load on background newer items, that are loaded to top, without making you scroll. Lastly, you can go top and pull to refresh and it will download new items(there is difference in twitter) and items are added on top of your position.

So 3 triggers:

  1. Scroll down to load more with indicatior
  2. Pull to refresh
  3. Automatical refresh on background

No one from those should make you change position

Dominik Šimoník
  • 1,442
  • 10
  • 17
  • 1
    When new items added you need stay at current position right? – Yashraj Aug 29 '22 at 10:59
  • 1
    @Yashraj yes both on top and bottom. – Dominik Šimoník Aug 29 '22 at 11:04
  • 1
    Does this thread help you in any way? https://stackoverflow.com/questions/49153087/flutter-scrolling-to-a-widget-in-listview – Karolina Hagegård Aug 29 '22 at 12:28
  • 1
    @KarolinaHagegård I dont have problem with scrolling and I have problem with updating number of items in list. I can do scrollTo and jumpTo at the same time when im adding new items to list but seems like a hack to me and its not working 100% time, when your item also does not have fixed height all the time – Dominik Šimoník Aug 29 '22 at 14:04
  • 1
    But you said you want the item to stay in the same place on the screen, but it doesn't? Isn't that a problem with scrolling? How about the `ScrollablePositionedList()` for example, as this answer suggests? https://stackoverflow.com/a/58924218/14335655 – Karolina Hagegård Aug 29 '22 at 14:20
  • @KarolinaHagegård It does. Check the answer of yellowgray, we probably dont understand each other what is the expected behavior. He is close, but on refresh he does have extra scroll to latest that i dont want. I tried scrollable position list and on refresh i did jumpTo to index where i started my refresh, but it wasnt reliable – Dominik Šimoník Aug 31 '22 at 10:50

2 Answers2

1

Because the solution is quite different and required a package scrollable_positioned_list, I add another answer here.

The first requirement is that you assume the child's height is flexible. So I suggest using scrollable_positioned_list then you can scroll to index without knowing the concrete scroll position.

The second requirement is that you want to load new items from the start of the list while keeping the position. It is not possible by using only listview because the listview always counts items from index 0. So you need the external function like jumpTo, position.correctPixels, or something else that can make the position right after inserting items from index 0.

Sadly the scrollable_positioned_list can only use jumpTo to change the position: it will interrupt scrolling. If you think it is a problem, you may need to implement a custom "scrollable positioned list" which supports some functions like correctPixels which won't interrupt the user's scrolling.

Sample Code

I move the list into the widget because it needs full control of every item.

MyPostService

int countTop = -1;
int countBottom = 21;
class MyPostService {
  static Future<List<String>> fetchNew() async {
    await Future.delayed(const Duration(seconds: 1));
    final result = List.generate(5, (index) => '*post ${countTop + index - 4}');
    countTop -= 5;
    return result;
  }

  static Future<List<String>> loadMore() async {
    await Future.delayed(const Duration(seconds: 1));
    final result = List.generate(5, (index) => '*post ${countBottom + index}');
    countBottom += 5;
    return result;
  }
}

TestPositionedList

class TestPositionedList extends StatefulWidget {
  const TestPositionedList({
    required this.initialList,
    Key? key,
  }) : super(key: key);

  final List<String> initialList;

  @override
  State<TestPositionedList> createState() => _TestPositionedListState();
}

class _TestPositionedListState extends State<TestPositionedList> {
  final itemScrollController = ItemScrollController();
  final itemPositionsListener = ItemPositionsListener.create();

  /// list items
  List<String?> posts = [];

  /// Current index and offset
  int currentIndex = 0;
  double itemLeadingEdge = 0.0;

  /// Show loading indicator
  bool isLoadingMore = false;

  @override
  void initState() {
    posts = widget.initialList;
    itemPositionsListener.itemPositions.addListener(() async {
      if (posts.isNotEmpty) {
        /// Save current index and offset
        final firstItem = itemPositionsListener.itemPositions.value.first;
        currentIndex = firstItem.index;
        itemLeadingEdge = firstItem.itemLeadingEdge;

        /// load more
        final lastItem = itemPositionsListener.itemPositions.value.last;
        if (lastItem.itemTrailingEdge < 1) {
          await _onLoadMore();
        }
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final finalList = [
      ...posts,
      if (isLoadingMore) null,
    ];

    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ScrollablePositionedList.builder(
        itemScrollController: itemScrollController,
        itemPositionsListener: itemPositionsListener,
        itemCount: finalList.length,
        itemBuilder: (_, index) {
          final post = finalList[index];
          if (post != null) {
            return ListTile(title: Text(post));
          } else {
            return _loadingIndicator();
          }
        },
      ),
    );
  }

  Future _onRefresh() async {
    final newPosts = await MyPostService.fetchNew();
    final firstItem = itemPositionsListener.itemPositions.value.first;
    setState(() {
      posts.insertAll(0, newPosts);
      itemScrollController.jumpTo(
        index: firstItem.index + newPosts.length,
        alignment: firstItem.itemLeadingEdge,
      );
    });
  }

  Future _onLoadMore() async {
    if (!isLoadingMore) {
      setState(() => isLoadingMore = true);
      final morePosts = await MyPostService.loadMore();
      setState(() {
        posts.addAll(morePosts);
        isLoadingMore = false;
      });
    }
  }

  Widget _loadingIndicator() {
    return const Center(
      child: Padding(
        padding: EdgeInsets.symmetric(vertical: 20),
        child: CircularProgressIndicator(),
      ),
    );
  }
}
yellowgray
  • 4,006
  • 6
  • 28
1

I have managed to do fork o scrollable_positioned_list to, no jumping through itemScrollControlled is needed.

I have added to scrollable_positioned_list two parameters

  final bool keepPosition;
  final String Function(int index)? onItemKey;

On build I have added

    if (widget.itemCount > 0 && widget.onItemKey != null) {
      _lastTargetKey = widget.onItemKey!(primary.target);
    } else {
      _lastTargetKey = null;
    }

Extra search for indexes

  String? _lastTargetKey;
  int? _getIndexOfKey() {
    if (widget.onItemKey != null) {
      int? index;
      for (var i = 0; i < widget.itemCount; i++) {
        if (widget.onItemKey!(i) == _lastTargetKey) {
          index = i;
          break;
        }
      }
      return index;
    }
  }

And added to didUpdateWidget

    if (widget.keepPosition) {
      if (_lastTargetKey != null) {
        var foundIndex = _getIndexOfKey();
        if (foundIndex != null && foundIndex > primary.target) {
          primary.target = foundIndex;
        }
      }
    }
Dominik Šimoník
  • 1,442
  • 10
  • 17