I am trying to update my list of Thread objects when I reach near the end of the screen while scrolling down (to show an infinite list of items while I keep scrolling down)
My current setup is the following:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'dart:convert';
import 'forums.dart';
// Retrieve JSON response forum thread list
Future<List<Thread>> fetchForumThreadList(String url, int page) async {
final response =
await http.get('http://10.0.2.2:8080/frest$url/page/$page');
if (response == null) {
throw new Exception("No site");
}
if (response.statusCode == 200) {
return compute(parseForumThreadList, response.body);
} else {
List<Thread> e = [];
return e;
}
}
List<Thread> parseForumThreadList(String responseBody) {
Map decoded = json.decode(responseBody);
List<Thread> threads = [];
Map threadList = decoded["list"];
for (var thread in threadList["List"]) {
threads.add(Thread(
thread["ID"],
thread["Staff"],
thread["Support"],
thread["Sticky"],
thread["Locked"],
thread["Title"],
thread["Replies"],
thread["Views"],
thread["Author"],
thread["CreatedAt"],
));
}
return threads;
}
// Generate a card list from a List of forum threads
Widget generateForumThreadList(BuildContext context, int index, List<Thread> data) {
// Use custom icon for staff posts
IconData authorIcon = Icons.account_circle;
Color authorIconColor = Color(0xAAFFFFFF);
if (data[index].staff) {
authorIcon = Icons.verified_user;
authorIconColor = Color(0xAAFFBA08);
}
return Column(
children: <Widget>[
InkWell(
onTap: () {
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
// We need to wrap columns under Flexible
// To make text wrap if larger than screen width
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 6.0),
child: Text(
data[index].title,
style: TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 2.0),
// Add thread author and created date
child: Row(
children: <Widget>[
Icon(
authorIcon,
size: 15.0,
color: authorIconColor,
),
Padding(
padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
child: Text(
"Author: " + data[index].author,
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 12.0),
// Add threads and posts information
child: Row(
children: <Widget>[
Icon(
Icons.chat_bubble,
size: 15.0,
color: Color(0xAAFFFFFF),
),
Padding(
padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
child: Text(
data[index].replies.toString() + " Replies",
),
),
Icon(
Icons.pageview,
size: 15.0,
color: Color(0xAAFFFFFF),
),
Padding(
padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
child: Text(
data[index].views.toString() + " Views",
),
),
],
),
),
],
),
),
],
),
),
// Add a divider for each forum item
Divider(
height: 4.0,
),
],
);
}
// Class used for a forum thread
class Thread {
final int id;
final bool staff;
final bool support;
final bool sticky;
final bool locked;
final String title;
final int replies;
final int views;
final String author;
final String createdAt;
Thread(
this.id,
this.staff,
this.support,
this.sticky,
this.locked,
this.title,
this.replies,
this.views,
this.author,
this.createdAt,
);
}
class ForumThreadList extends StatefulWidget {
final Forum forum;
ForumThreadList(this.forum);
@override
_ForumThreadListState createState() => _ForumThreadListState(forum);
}
class _ForumThreadListState extends State<ForumThreadList> {
final Forum forum;
int page = 1;
ScrollController controller;
_ForumThreadListState(this.forum);
@override
void initState() {
controller = new ScrollController();
controller.addListener(_scrollListener);
super.initState();
}
@override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
VoidCallback _scrollListener() {
// If we are near the end of the list
if (controller.position.extentAfter < 300) {
page++;
print(page);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(forum.name),
),
body: Scrollbar(
child: Center(
child: FutureBuilder<List<Thread>>(
future: fetchForumThreadList(forum.url, page),
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
controller: controller,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext ctx, int index) {
return generateForumThreadList(ctx, index, snapshot.data);
},
);
} else if (snapshot.hasError) {
return Text(snapshot.error);
}
return CircularProgressIndicator();
},
),
),
),
// Add floating button to reload forums
floatingActionButton: new FloatingActionButton(
elevation: 0.0,
child: Icon(
Icons.sync,
size: 32.0,
),
// When pressing the button reload the forum list
onPressed: () { },
),
);
}
}
Right now everything works fine, when I load the application the first page of my API is fetched and the list is populated, however I cant figure how to append the next page of elements when I am near the end of the screen.
I tried updating my page variable when near the end thinking that this would make the FutureBuilder update, but this does not seem to be correct and I also think this wont give me my desired result (making the list expand instead of replacing items with a new batch).
What you're looking for seems to be List pagination. Checking your repro, it's unable to trigger a callback when the scroll is at the end of the List. Instead of _scrollListener(), you can add this listener for the ScrollController.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0)
debugPrint('List scroll at top');
else {
debugPrint('List scroll at bottom');
// Scroll is at the end of the page, load next page
loadMoreImages(true);
}
}
});
Once the bottom of the page is hit, call a method that should add more items on the ListView.
// Succeeding pages will display 3 more items from the List
loadMoreImages(bool increment) {
setState(() {
if (!increment)
// if increment is set to false
// List will only show first page
_listCursorEnd = 3;
else
// else, add items to load next page
_listCursorEnd += 3;
});
}
On the subject of using StreamBuilder, one way to utilize this feature is to set the List items in the StreamController.
var _streamController = StreamController<List<Album>>();
To add content on the StreamController, use StreamController.add()
fetchAlbum().then((response) => _streamController.add(response));
Then add the StreamBuilder on Widget build() and use the snapshot data from StreamBuilder to populate the ListView.
StreamBuilder(
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<List<Album>> snapshot) {
if (snapshot.hasData) {
// This ensures that the cursor won't exceed List<Album> length
if (_listCursorEnd > snapshot.data.length)
_listCursorEnd = snapshot.data.length;
}
return Widget(); // Populate ListView widget using snapshot data
},
);
Here's the complete code. The sample also demonstrates a "pull to refresh" function as a bonus.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _streamController = StreamController<List<Album>>();
var _scrollController = ScrollController();
// Succeeding pages should display 3 more items from the List
loadMoreImages(bool increment) {
setState(() {
if (!increment)
_listCursorEnd = 3;
else
_listCursorEnd += 3;
});
}
// Call to fetch images
loadImages(bool refresh) {
fetchAlbum().then((response) => _streamController.add(response));
if (refresh) loadMoreImages(!refresh); // refresh whole List
}
@override
void initState() {
super.initState();
loadImages(false);
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0)
print('List scroll at top');
else {
print('List scroll at bottom');
loadMoreImages(true);
}
}
});
}
@override
void dispose() {
super.dispose();
_streamController.close();
}
var _listCursorEnd = 21;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<List<Album>> snapshot) {
if (snapshot.hasData) {
// This ensures that the cursor won't exceed List<Album> length
if (_listCursorEnd > snapshot.data.length)
_listCursorEnd = snapshot.data.length;
debugPrint('Stream snapshot contains ${snapshot.data.length} item/s');
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: RefreshIndicator(
// onRefresh is a RefreshCallback
// RefreshCallback is a Future Function().
onRefresh: () async => loadImages(true),
child: snapshot.hasData
? ListView.builder(
controller: _scrollController,
primary: false,
padding: const EdgeInsets.all(20),
itemBuilder: (context, index) {
if (index < _listCursorEnd) {
return Container(
padding: const EdgeInsets.all(8),
child: Image.network(
snapshot.data[index].albumThumbUrl,
fit: BoxFit.cover),
// child: Thumbnail(image: imagePath, size: Size(100, 100)),
);
} else
return null;
},
)
: Text('Waiting...'),
),
),
);
},
);
}
Future<List<Album>> fetchAlbum() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/photos');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
Iterable iterableAlbum = json.decode(response.body);
var albumList = List<Album>();
List<Map<String, dynamic>>.from(iterableAlbum).map((Map model) {
// Add Album mapped from json to List<Album>
albumList.add(Album.fromJson(model));
}).toList();
return albumList;
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
getListImg(List<Album> listAlbum) {
var listImages = List<Widget>();
for (var album in listAlbum) {
listImages.add(
Container(
padding: const EdgeInsets.all(8),
child: Image.network(album.albumThumbUrl, fit: BoxFit.cover),
// child: Thumbnail(image: imagePath, size: Size(100, 100)),
),
);
}
return listImages;
}
}
class Album {
final int albumId;
final int id;
final String title;
final String albumImageUrl;
final String albumThumbUrl;
Album(
{this.albumId,
this.id,
this.title,
this.albumImageUrl,
this.albumThumbUrl});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
albumId: json['albumId'],
id: json['id'],
title: json['title'],
albumImageUrl: json['url'],
albumThumbUrl: json['thumbnailUrl'],
);
}
}

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With