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.
. . .
Dynamic data loading
First, we need to create a 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:
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:
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 the request is being done.
. . .
Progress Indicator
Our main widget will be CircularProgressIndicator
, which will be wrapped in Center
, Opacity
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:
The last thing is to add this widget to our ListView:
The final solution should look like this:
. . .
Handling empty data
As a bonus, I will show you a 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
:
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
Marcin Szałek
Founder of Fidev
Flutter enthusiast since Alpha release in 2017. Believes that sharing is caring, which lead him to start a technical blog dedicated fo Flutter in its early days. Loves to see beautiful designs become real apps and is willing to help make it happen. Enjoys sunny beaches far from home.
Join the newsletter!
Join the newsletter to keep track with latest posts and get my special Widget to animate views' entrances without any hassle for FREE!
Check out other posts!
Stripe Checkout in Flutter Web
Flutter Web is getting more mature every day. If you want to accept payments using Stripe Checkout in your Flutter web application, this article is just for you!
Stripe Checkout in mobile Flutter app
Have you ever struggled to integrate card payments into your mobile Flutter app? If so, today is your lucky day! In this post, I present how to use Stripe Checkout in the Flutter app without any hassle!
Interacting with Widgets using Framy
Have you ever developed a widget or a page and you wanted to make sure it works correctly in different scenarios but then it turned out that you can’t just reproduce all the cases you want to cover? Framy may solve such problems!
Amazing post~
So why edge is 50.0 ? How did you get that number?
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.
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
Hm…
I’m sorry but I have no idea why it would happen ;(
Nice article.
Great post, how i can do with json?
Hi, you may find this helpful: https://www.youtube.com/watch?v=-PRrdG163to
Thanks mszalek.
It helped me..
You’re welcome, I’m glad it did 🙂
Nice article, thanks!
Any ideas to handle errors with retry?
Sorry my english.
Hi man! What exactly are you looking for? 🙂
I did something like that:
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest || hasError ? 1.0 : 0.0,
child: hasError
? new IconButton(
icon: Icon(
Icons.refresh,
color: AppColors.accentColor,
size: 30.0,
),
onPressed: _getMoreData)
: new SpinKitWave(
color: AppColors.accentColor,
size: 30.0,
),
),
),
);
}
Hi , nice article..if in _buildProgressIndicator i set isPeformingRequest to true always, why does it still behave correctly i am amazed a bit ? can you throw some light on it?
Hey man, can you share a gist snippet with a code for that? 🙂
Nice work bro. I like the simple elegance
Thanks 🙂 I’m glad you liked it 🙂
Good job man!
Thank you! 🙂
Hello,
I am working on the infinite list with StreamBuilder.
Here’s how the code goes (to load a list of podcasts):
return StreamBuilder(
stream: podcastsBloc.podcasts,
builder: (context, snapshot) {
isLoading = false;
if (snapshot.hasData) {
return ListView.builder(
scrollDirection: Axis.vertical,
padding: EdgeInsets.zero,
controller: controller,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return RecentPodcastListItem(snapshot.data[index]);
});
} else if (snapshot.hasError) {
//SHOW ERROR TEXT
return Text(“Error!”);
} else {
//LOADER GOES HERE
return Text(
“Loading…”,
style: TextStyle(color: Colors.white),
);
}
},
);
//////And here’s how the code of it’s stateful widget goes:
ScrollController controller = ScrollController();
PodcastsBloc podcastsBloc;
bool isLoading = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
podcastsBloc = PodcastsProvider.of(context);
podcastsBloc.getPodcasts();
controller.addListener((){
if(controller.position.maxScrollExtent – controller.position.pixels <= 200 && !isLoading){
setState(() {
isLoading = true;
podcastsBloc.getPodcasts();
});
}
});
}
/////Is this a correct way to do things?
Hey, to be honest it’s hard for me to tell you anything based on what you posted, if you can give me link to repo or gist where I can see more code, maybe I will be able to help 🙂
Very nice example.
But i think you can update _buildProgressIndicator return empty view, it will not show in bottom, dont need use Opacity.
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(child: isLoadingBottom? new CircularProgressIndicator(): new Container(),
),
);
}
Hey, the goal was to have progress indicator take space, if it didn’t you would scroll to the bottom and then the indicator would appear under all children and you wouldn’t see it unless you scroll even more. Having opacity, it when you scroll to the bottom, you have to scroll to the place where you can potentially see the indicator.
Thanks so much, i love your codes. But, can this work if i am retrieving my data from the internet. Thanks
Of course! Just replace fakeRequest method with actual request 😉
is it possible to add a RefreshIndicator in the listview without breaking the scrollcontroller?
hi, thanks very mush;
What should we do if we want to display the “CircularProgressIndicator” for the first time before loading the list?
For example, when information is downloaded from the Internet.
Show it instead of list 🙂
thank you, it worked like a charm.
🙂
Trying to make network request. But don’t know how to handle error. Any idea please.
Have Replace
Future<List> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to – from, (i) => i + from);
});
}
With
Future<List> makeRequest() async {
List records = [];
response = await client.get(pageUrl);
Iterable list = convert.json.decode(response.body);
records = list.map((model) => Post.fromJson(model)).toList();
return records;
}
Hey, sorry for late reply. Did you manage to do it? 🙂
Czesc Marcin dzieki bardzo fajny przyklad
🙂
Tried to add circular progress indicator with dynamic listview using stream builder with above logic to show the progress below the listview but it is not getting displayed… I think the logic is wrong…. As the index starts from 0 and length starts from 1 so it is not showing progress as that condition will never meet.
Please explain me how it will work when i am using it with stream builder and bloc pattern together.
Well, it should work the same…
You can send me gist with the code and maybe I will find an error 🙂
Hi and sorry for adding this issue here. The issue was simply not returning the buildProgressBar Function from inside the if condition in the stream builder. i was returning the list data but not circular progress function. Your logic is proper and correct and i am sorry for wasting your precious time for replying me here. If you want then you can remove my comments from the blog as well.