ListView Lazy Loading in Up and Down Direction

You want to create a ListView in flutter with lazy loading in up and down direction with boundaries?
Example: You want to display a scoreboard with 100.000 entries and want to start displaying items from 50.000. New entries should load automatically if scrolling up or down until entry 0 or entry 100.000 is reached (boundaries).

If yes, awesome, then that’s the right article for you.

There are multiple tutorials available explaining how to implement lazy loading with flutter. Unfortunately, I couldn’t find a guide so far which explains how to create a ListView which supports lazy loading in the up and down direction with specified boundaries. Therefore, I decided to create this article.

Let’s explain it with an example. The goal is to create the following list view:

ListView with lazy loading in up and down direction
ListView with lazy loading in up and down direction

1. Add the library bidirectional_listview to your project

Luckily, there’s already a library called bidirectional_listview which we’ll use for lazy loading in the up and down direction with boundaries.

Therefore, integrate bidirectional_listview into your project by adding it to pubspec.yaml:

dependencies:
  bidirectional_listview: ^1.0.1+1

2. Create an initial BidirectionalListView Widget

Let’s create an initial widget which contains a BidirectionalListView, first, without lazy loading.

Therefore, let’s create a simple class MyHome.dart. Please note: Executing this code will throw an error, because scroll boundaries have to be set. This will be added in step 3.

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

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

  @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) {
   // find out item counts
    List<int> keys = items.keys.toList();
    keys.sort();
    int negativeItemCount = keys.first;
    int itemCount = keys.last;

    return new Scaffold(
      body: new Scrollbar(
        child: new BidirectionalListView.builder(
          controller: controller,
          physics: AlwaysScrollableScrollPhysics(),
          itemBuilder: (context, index) {
            return Container(
                child: Text(items[index]),
                height: kItemHeight,
                padding: EdgeInsets.all(0),
                margin: EdgeInsets.all(0));
          },
          itemCount: itemCount,
          negativeItemCount: negativeItemCount.abs(),
        ),
      ),
    );
  }

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

  // Reload new items in up and down direction and update scroll boundaries
  void _scrollListener() {
    // Will be filled in next step
  }
}

3. Add lazy loading in up and down direction and scroll boundaries

Eventually, you already noticed that the method _scrollListener wasn’t filled in step 2.

In this step, we’re filling the _scrollListener method in order to load new list view items and to set scroll boundaries.

  // Reload new items in up and down direction and update scroll boundaries
  void _scrollListener() {
    // detect if scrolling up or down
    bool scrollingDown = oldScrollPosition < controller.position.pixels;

    // find out item counts
    List<int> keys = items.keys.toList();
    keys.sort();
    int negativeItemCount = keys.first.abs();
    int itemCount = keys.last;

    // calculate scroll border from where to load new items
    double positiveReloadBorder = (itemCount * kItemHeight - 3 * kItemHeight);
    double negativeReloadBorder =
        (-(negativeItemCount * kItemHeight - 3 * kItemHeight));
    
    // reload items
    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;
    }

    // set new scroll boundaries
    try {
      BidirectionalScrollPosition pos = controller.position;
      pos.setMinMaxExtent(
          -negativeItemCount * kItemHeight, itemCount * kItemHeight);
    } catch (error) {
      print(error.toString());
    }
    if (rebuildNecessary) {
      _rebuild();
    }

    oldScrollPosition = controller.position.pixels;
  }

Finally, that’s it :-).

Sources: