Infinite Dynamic ListView

Infinite Dynamic ListView

In this post, I will quickly go through how to make an infinite ListView, that dynamically loads more data when a user scrolls down to the end. The final solution should look like this:

Let’s get to it!

Starting point

Let’s start with a simple list of 10 integer elements.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);

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

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: new Text("Number $index"));
        },
      ),
    );
  }
}

Dynamic data loading

First, we need to create method imitating an http request. Let’s assume that we can pass from and to parameters and as a result, we get items between them. We will also add some delay to make this method more network-ish. It can look like this:

/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
 return Future.delayed(Duration(seconds: 2), () {
   return List.generate(to - from, (i) => i + from);
 });
}

We would like to call that method when the user scrolls to the end of theListView. The easiest way to do that is to attach ScrollController to it. ScrollController will listen for scrolling behavior and it will make a request when the user scrolls to the end. When it comes to making requests, it is important to prevent our app from doing them too often (doing request before the previous one has finished). My solution to that problem is to add a flag isPerformingRequest and start a new request, only if the flag is set to false. Code for this step should look like this:

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);
  ScrollController _scrollController = new ScrollController();
  bool isPerformingRequest = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  _getMoreData() async {
    if (!isPerformingRequest) {
      setState(() => isPerformingRequest = true);
      List<int> newEntries = await fakeRequest(items.length, items.length + 10);
      setState(() {
        items.addAll(newEntries);
        isPerformingRequest = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: new Text("Number $index"));
        },
        controller: _scrollController,
      ),
    );
  }
}

If we run our app, we can see that items are being added dynamically. However, this solution is far from acceptable. We need to add some kind of indicator to inform the user that request is being done.

Progress Indicator

Our main widget will be CircularProgressIndicator,  which will be wrapped in CenterOpacity and Padding. We are going to use the Opacity widget to show our progress indicator only when the request is being performed. The whole widget should look like this:

Widget _buildProgressIndicator() {
  return new Padding(
    padding: const EdgeInsets.all(8.0),
    child: new Center(
      child: new Opacity(
        opacity: isPerformingRequest ? 1.0 : 0.0,
        child: new CircularProgressIndicator(),
      ),
    ),
  );
}

The last thing is to add this widget to our ListView:

@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: AppBar(
      title: Text("Infinite ListView"),
    ),
    body: ListView.builder(
      itemCount: items.length + 1,
      itemBuilder: (context, index) {
        if (index == items.length) {
          return _buildProgressIndicator();
        } else {
          return ListTile(title: new Text("Number $index"));
        }
      },
      controller: _scrollController,
    ),
  );
}

The final solution should look like this:

Handling empty data

As a bonus, I will show you simple way to handle a case, when no data is being returned from the request. All we need to do is to animate our ListView a bit using ScrollController:

_getMoreData() async {
  if (!isPerformingRequest) {
    setState(() => isPerformingRequest = true);
    List<int> newEntries = await fakeRequest(items.length, items.length); //returns empty list
    if (newEntries.isEmpty) {
      double edge = 50.0;
      double offsetFromBottom = _scrollController.position.maxScrollExtent - _scrollController.position.pixels;
      if (offsetFromBottom < edge) {
        _scrollController.animateTo(
            _scrollController.offset - (edge -offsetFromBottom),
            duration: new Duration(milliseconds: 500),
            curve: Curves.easeOut);
      }
    }
    setState(() {
      items.addAll(newEntries);
      isPerformingRequest = false;
    });
  }
}

Notice how we need to check if the user didn’t scroll up before response arrived. We are doing it by comparing edge with offsetFromBottom.

And that’s it, folks! 🙂

A gist containing whole class can be found here.

If you have any questions or suggestions on how to implement it better, I strongly encourage you to leave a comment.

Cheers 🙂

 

 

4 thoughts on “Infinite Dynamic ListView

    1. Hey,
      sorry for delayed reply, didn’t notice your comment.
      Just by trial and error method 🙂
      My guess is that it is supposed to be similar to the height of rows in your list.

  1. Great example – really useful, thank you.

    However, I am experiencing the following weird issue: the 1st time the scrolling reaches the end of the screen (and only the 1st time) and the items are being reloaded (I have integrated your code with my app), the list automatically scrolls to the beginning: afterwards, everything goes smooth, each reload is done properly.

    Any idea/hints which can help me debugging this issue?

    Thank you

Leave a Reply

Your email address will not be published.