当我向下滚动时到达屏幕末端附近时,我尝试更新Thread
对象的列表(在我不断向下滚动时显示无限的项目列表)
我当前的设置如下:
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: () { },
),
);
}
}
现在一切正常,当我加载应用程序时,获取了我的API的第一页并填充了列表,但是当我接近屏幕末尾时,我无法确定如何追加元素的下一页。
我在接近尾声时尝试更新page
变量,以为这将使FutureBuilder
更新,但这似乎并不正确,而且我也认为这不会给我我想要的结果(使列表扩大,而不是用新批次替换项目。
答案 0 :(得分:2)
您正在寻找的似乎是列表分页。检查您的重现,当滚动位于列表末尾时无法触发回调。您可以为 _scrollListener()
添加此侦听器,而不是 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);
}
}
});
点击页面底部后,调用应该在 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;
});
}
关于使用 StreamBuilder,利用此功能的一种方法是在 StreamController
中设置列表项。
var _streamController = StreamController<List<Album>>();
要在 StreamController 上添加内容,请使用 StreamController.add()
fetchAlbum().then((response) => _streamController.add(response));
然后在 Widget build()
上添加 StreamBuilder,并使用来自 StreamBuilder 的快照数据来填充 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
},
);
这是完整的代码。该示例还演示了作为奖励的“拉动刷新”功能。
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'],
);
}
}