Flutter ListView lazy loading in both directions (up, down)

旧时模样 提交于 2020-05-15 21:48:48

问题


I would like to have a ListView in flutter which provides lazy loading in both directions (up, down).

Example:

  • There are 60000 items in the backend database which can be theoretically displayed.
  • First I want to display the items 100..120
  • From these indices I want to be able to scroll up and down while lazy loading the new items

Things to consider:

  • The top and bottom edge (current index < 0 or > 60000) should be bouncing if reached while scrolling

What I've tried:

  • Most of the approaches in Flutter ListView lazy loading. These solutions just worked for lazy loading in one direction (e.g. down or up if list is reversed). If scrolling into the other direction, the list view "jumped" because indices changed (the old index 1 isn't the new index 1 anymore).
  • ScrollablePositionedList (https://pub.dev/documentation/flutter_widgets/latest/flutter_widgets/ScrollablePositionedList-class.html): Here the issue was that the widget wants to load every item if e.g. an itemcount of 60000 is given. Anyhow, the itemcount is required in order that this solution works properly.
  • IndexedListView (https://pub.dev/packages/indexed_list_view): Same issue as in ScrollablePositionedList. Anyhow, here also the "bouncing" at the top and bottom of the list didn't work (because of missing scroll extents).
  • InfiniteListView (https://github.com/fluttercommunity/flutter_infinite_listview): Same issue as in IndexedListView

I hope that here are some pretty smart people who can help me solving this issue ;). I'm already searching and trying around since days on this issue. Thanks!

Update To make things clearer: Here is an example for a ListView with lazy loading for scrolling up and down (Most of the code copied from https://stackoverflow.com/a/49509349/10905712 by Rémi Rousselet):

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => new _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  ScrollController controller;
  List<String> items = new List.generate(100, (index) => 'Hello $index');

  @override
  void initState() {
    super.initState();
    controller = new ScrollController()..addListener(_scrollListener);
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Scrollbar(
        child: new ListView.builder(
          controller: controller,
          itemBuilder: (context, index) {
            return new Text(items[index]);
          },
          itemCount: items.length,
        ),
      ),
    );
  }

  double oldScrollPosition = 0.0;
  void _scrollListener() {
    bool scrollingDown = oldScrollPosition < controller.position.pixels;
    print(controller.position.extentAfter);

    if (controller.position.extentAfter < 500 && scrollingDown) {
      setState(() {
        items.addAll(new List.generate(
            42, (int index) => Random().nextInt(10000).toString()));
      });
    } else if (controller.position.extentBefore < 500 && !scrollingDown) {
      setState(() {
        items.insertAll(
            0,
            new List.generate(
                42, (index) => Random().nextInt(10000).toString()));
      });
    }

    oldScrollPosition = controller.position.pixels;
  }
}

If you execute this code and try scrolling up, you'll see a "jumping" in the list. Scrolling down + lazy load works perfectly. Scrolling up + lazy load would work if the ListView would be reversed. Anyhow, with this solution we would have the same issue with scrolling down + lazy load here.


回答1:


Update

I just created a new library bidirectional_listview which can be used to solve this issue. BidirectionalListView is a fork from infinite_listview.

Old Answer

I just solved it by adapting the library InfiniteListView a bit. I had to extend a setter for minScrollExtent and maxScrollExtent. In addition, I added a separate count for negative indices:

library infinite_listview;

import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class BidirectionalListView extends StatelessWidget {
  /// See [ListView.builder]
  BidirectionalListView.builder({
    Key key,
    this.scrollDirection = Axis.vertical,
    BidirectionalScrollController controller,
    this.physics,
    this.padding,
    this.itemExtent,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    int negativeItemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    this.anchor = 0.0,
    this.cacheExtent,
  })  : positiveChildrenDelegate = SliverChildBuilderDelegate(
          itemBuilder,
          childCount: itemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        negativeChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) => itemBuilder(context, -1 - index),
          childCount: negativeItemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        controller = controller ?? BidirectionalScrollController(),
        super(key: key);

  /// See [ListView.separated]
  BidirectionalListView.separated({
    Key key,
    this.scrollDirection = Axis.vertical,
    BidirectionalScrollController controller,
    this.physics,
    this.padding,
    @required IndexedWidgetBuilder itemBuilder,
    @required IndexedWidgetBuilder separatorBuilder,
    int itemCount,
    int negativeItemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    this.cacheExtent,
    this.anchor = 0.0,
  })  : assert(itemBuilder != null),
        assert(separatorBuilder != null),
        itemExtent = null,
        positiveChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            final itemIndex = index ~/ 2;
            return index.isEven
                ? itemBuilder(context, itemIndex)
                : separatorBuilder(context, itemIndex);
          },
          childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        negativeChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            final itemIndex = (-1 - index) ~/ 2;
            return index.isOdd
                ? itemBuilder(context, itemIndex)
                : separatorBuilder(context, itemIndex);
          },
          childCount: negativeItemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        controller = controller ?? BidirectionalScrollController(),
        super(key: key);

  /// See: [ScrollView.scrollDirection]
  final Axis scrollDirection;

  /// See: [ScrollView.controller]
  final BidirectionalScrollController controller;

  /// See: [ScrollView.physics]
  final ScrollPhysics physics;

  /// See: [BoxScrollView.padding]
  final EdgeInsets padding;

  /// See: [ListView.itemExtent]
  final double itemExtent;

  /// See: [ScrollView.cacheExtent]
  final double cacheExtent;

  /// See: [ScrollView.anchor]
  final double anchor;

  /// See: [ListView.childrenDelegate]
  final SliverChildDelegate negativeChildrenDelegate;

  /// See: [ListView.childrenDelegate]
  final SliverChildDelegate positiveChildrenDelegate;

  @override
  Widget build(BuildContext context) {
    final List<Widget> slivers = _buildSlivers(context, negative: false);
    final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
    final AxisDirection axisDirection = _getDirection(context);
    final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
    return Scrollable(
      axisDirection: axisDirection,
      controller: controller,
      physics: scrollPhysics,
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return Builder(builder: (BuildContext context) {
          /// Build negative [ScrollPosition] for the negative scrolling [Viewport].
          final state = Scrollable.of(context);
          final negativeOffset = BidirectionalScrollPosition(
            physics: scrollPhysics,
            context: state,
            initialPixels: -offset.pixels,
            keepScrollOffset: controller.keepScrollOffset,
            negativeScroll: true,
          );

          /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
          offset.addListener(() {
            negativeOffset._forceNegativePixels(offset.pixels);
          });

          /// Stack the two [Viewport]s on top of each other so they move in sync.
          return Stack(
            children: <Widget>[
              Viewport(
                axisDirection: flipAxisDirection(axisDirection),
                anchor: 1.0 - anchor,
                offset: negativeOffset,
                slivers: negativeSlivers,
                cacheExtent: cacheExtent,
              ),
              Viewport(
                axisDirection: axisDirection,
                anchor: anchor,
                offset: offset,
                slivers: slivers,
                cacheExtent: cacheExtent,
              ),
            ],
          );
        });
      },
    );
  }

  AxisDirection _getDirection(BuildContext context) {
    return getAxisDirectionFromAxisReverseAndDirectionality(
        context, scrollDirection, false);
  }

  List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
    Widget sliver;
    if (itemExtent != null) {
      sliver = SliverFixedExtentList(
        delegate:
            negative ? negativeChildrenDelegate : positiveChildrenDelegate,
        itemExtent: itemExtent,
      );
    } else {
      sliver = SliverList(
          delegate:
              negative ? negativeChildrenDelegate : positiveChildrenDelegate);
    }
    if (padding != null) {
      sliver = new SliverPadding(
        padding: negative
            ? padding - EdgeInsets.only(bottom: padding.bottom)
            : padding - EdgeInsets.only(top: padding.top),
        sliver: sliver,
      );
    }
    return <Widget>[sliver];
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
    properties.add(new DiagnosticsProperty<ScrollController>(
        'controller', controller,
        showName: false, defaultValue: null));
    properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
        showName: false, defaultValue: null));
    properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
        'padding', padding,
        defaultValue: null));
    properties
        .add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
    properties.add(
        new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
  }
}

/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class BidirectionalScrollController extends ScrollController {
  /// Creates a new [BidirectionalScrollController]
  BidirectionalScrollController({
    double initialScrollOffset = 0.0,
    bool keepScrollOffset = true,
    String debugLabel,
  }) : super(
          initialScrollOffset: initialScrollOffset,
          keepScrollOffset: keepScrollOffset,
          debugLabel: debugLabel,
        );

  @override
  ScrollPosition createScrollPosition(ScrollPhysics physics,
      ScrollContext context, ScrollPosition oldPosition) {
    return new BidirectionalScrollPosition(
      physics: physics,
      context: context,
      initialPixels: initialScrollOffset,
      keepScrollOffset: keepScrollOffset,
      oldPosition: oldPosition,
      debugLabel: debugLabel,
    );
  }
}

class BidirectionalScrollPosition extends ScrollPositionWithSingleContext {
  BidirectionalScrollPosition({
    @required ScrollPhysics physics,
    @required ScrollContext context,
    double initialPixels = 0.0,
    bool keepScrollOffset = true,
    ScrollPosition oldPosition,
    String debugLabel,
    this.negativeScroll = false,
  })  : assert(negativeScroll != null),
        super(
          physics: physics,
          context: context,
          initialPixels: initialPixels,
          keepScrollOffset: keepScrollOffset,
          oldPosition: oldPosition,
          debugLabel: debugLabel,
        ) {
    if (oldPosition != null &&
        oldPosition.minScrollExtent != null &&
        oldPosition.maxScrollExtent != null) {
      _minScrollExtent = oldPosition.minScrollExtent;
      _maxScrollExtent = oldPosition.maxScrollExtent;
    }
  }

  final bool negativeScroll;

  void _forceNegativePixels(double value) {
    super.forcePixels(-value);
  }

  @override
  double get minScrollExtent => _minScrollExtent;
  double _minScrollExtent = 0.0;

  @override
  double get maxScrollExtent => _maxScrollExtent;
  double _maxScrollExtent = 0.0;

  void setMinMaxExtent(double minExtent, double maxExtent) {
    _minScrollExtent = minExtent;
    _maxScrollExtent = maxExtent;
  }

  @override
  void saveScrollOffset() {
    if (!negativeScroll) {
      super.saveScrollOffset();
    }
  }

  @override
  void restoreScrollOffset() {
    if (!negativeScroll) {
      super.restoreScrollOffset();
    }
  }
}

The following example demonstrates lazy loading with scroll boundaries in both directions, up and down:

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tiverme/ui/helpwidgets/BidirectionalListView.dart';

class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => new _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  BidirectionalScrollController controller;
  Map<int, String> items = new Map();
  static const double ITEM_HEIGHT = 30;

  @override
  void initState() {
    super.initState();

    for (int i = -10; i <= 10; i++) {
      items[i] = "Item " + i.toString();
    }
    controller = new BidirectionalScrollController()
      ..addListener(_scrollListener);
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    List<int> keys = items.keys.toList();
    keys.sort();

    int negativeItemCount = keys.first;
    int itemCount = keys.last;
    print("itemCount = " + itemCount.toString());
    print("negativeItemCount = " + negativeItemCount.abs().toString());
    return new Scaffold(
      body: new Scrollbar(
        child: new BidirectionalListView.builder(
          controller: controller,
          physics: AlwaysScrollableScrollPhysics(),
          itemBuilder: (context, index) {
            return Container(
                child: Text(items[index]),
                height: ITEM_HEIGHT,
                padding: EdgeInsets.all(0),
                margin: EdgeInsets.all(0));
          },
          itemCount: itemCount,
          negativeItemCount: negativeItemCount.abs(),
        ),
      ),
    );
  }

  void _rebuild() => setState(() {});

  double oldScrollPosition = 0.0;
  void _scrollListener() {
    bool scrollingDown = oldScrollPosition < controller.position.pixels;
    List<int> keys = items.keys.toList();
    keys.sort();
    int negativeItemCount = keys.first.abs();
    int itemCount = keys.last;

    double positiveReloadBorder = (itemCount * ITEM_HEIGHT - 3 * ITEM_HEIGHT);
    double negativeReloadBorder =
        (-(negativeItemCount * ITEM_HEIGHT - 3 * ITEM_HEIGHT));

    print("pixels = " + controller.position.pixels.toString());
    print("itemCount = " + itemCount.toString());
    print("negativeItemCount = " + negativeItemCount.toString());
    print("minExtent = " + controller.position.minScrollExtent.toString());
    print("maxExtent = " + controller.position.maxScrollExtent.toString());
    print("positiveReloadBorder = " + positiveReloadBorder.toString());
    print("negativeReloadBorder = " + negativeReloadBorder.toString());

    bool rebuildNecessary = false;
    if (scrollingDown && controller.position.pixels > positiveReloadBorder) {
      for (int i = itemCount + 1; i <= itemCount + 20; i++) {
        items[i] = "Item " + i.toString();
      }
      rebuildNecessary = true;
    } else if (!scrollingDown &&
        controller.position.pixels < negativeReloadBorder) {
      for (int i = -negativeItemCount - 20; i < -negativeItemCount; i++) {
        items[i] = "Item " + i.toString();
      }
      rebuildNecessary = true;
    }

    try {
      BidirectionalScrollPosition pos = controller.position;
      pos.setMinMaxExtent(
          -negativeItemCount * ITEM_HEIGHT, itemCount * ITEM_HEIGHT);
    } catch (error) {
      print(error.toString());
    }
    if (rebuildNecessary) {
      _rebuild();
    }

    oldScrollPosition = controller.position.pixels;
  }
}


来源:https://stackoverflow.com/questions/59372391/flutter-listview-lazy-loading-in-both-directions-up-down

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!