Dominik Šimoník
Dominik Šimoník

Reputation: 1602

List with Refresh and LoadMore functions and deleting old

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

Upvotes: 7

Views: 3162

Answers (2)

Dominik Šimon&#237;k
Dominik Šimon&#237;k

Reputation: 1602

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;
        }
      }
    }

Upvotes: 1

yellowgray
yellowgray

Reputation: 4509

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(),
      ),
    );
  }
}

Upvotes: 1

Related Questions