Flutter - 如何更新用于构建 ListView 的 Future/List 的状态(或值?)(通过 FutureBuilder)
Flutter - How to update state (or value?) of a Future/List used to build ListView (via FutureBuilder)
我把相关代码贴在下面,但你可以根据我的伪解释来回答。
我正在使用 FutureBuilder 构建列表视图。
- 我首先使用 init() 异步 HTTP 调用 API 并将其解析为对象列表(位置)映射以表示 json 结果。
- 然后将位置列表返回到
Future<List<Location>> _listFuture
变量(这是 FutureBuilder 的未来)。
- 一旦未来 "returns" 或 "finishes" FutureBuilder 启动并使用 ListView.builder/Container/ListTile 循环遍历并构建列表。
- 在某些时候,我需要一个 onTap() 处理程序(在 ListTile 中)来更改所选列表项的背景颜色。
- 为了支持这一点,我在位置 class 中有一个 backgroundColor 成员(包含 JSON 响应),我默认为所有项目设置为“#fc7303”(假设一切都是初始状态未选中)。然后我想在 onTap() 中将所选内容的背景更改为“#34bdeb”。
- 我假设我可以调用 setState() 来触发刷新,并且新的背景颜色在重绘时将是 noticed/used。
问题是 ListView/Contrainer/ListTile 是由
驱动的
Future<List<Location>>
。我可以将 "tapped" 索引传递给我的 ontap 处理程序,但我不相信我可以让我的 _changeBackground() 只更新所选索引的 backgroundColor 值并调用 setState() 因为你不能直接 access/update a那样的未来(我收到错误 ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
)
我不确定我采取的方法是否正确。在这种情况下,我想我总是可以在理论上将 "background" 颜色跟踪分离到一个新的单独列表(在未来之外)并且 track/reference 使用来自 onTap() 的对齐索引。
但是,我不确定这是否总是有效。将来,我可能需要实际更改将来返回的 values/state 。例如,想想我是否希望能够单击列表项并更新 "companyName"。在这种情况下,我将直接更改存储在未来的值。我想我可以在技术上将新名称发送到服务器并以这种方式完全刷新列表,但这似乎效率低下(如果他们决定 "cancel" 而不保存更改怎么办?)。
感谢任何帮助。谢谢!
这个class实际上保存了列表的相关数据
// Location
class Location {
// members
String locationID;
String locationName;
String companyName;
String backgroundColor = 'fc7303';
// constructor?
Location({this.locationID, this.locationName, this.companyName});
// factory?
factory Location.fromJson(Map<String, dynamic> json) {
return Location(
locationID: json['locationID'],
locationName: json['locationName'],
companyName: json['companyName'],
);
}
}
此 class 是包含 "result" (success/error) 条消息的父 json 响应。它将上面的 class 实例化为一个列表来跟踪实际的 company/location 记录
//jsonResponse
class jsonResponse{
String result;
String resultMsg;
List<Location> locations;
jsonResponse({this.result, this.resultMsg, this.locations});
factory jsonResponse.fromJson(Map<String, dynamic> parsedJson){
var list = parsedJson['resultSet'] as List;
List<Location> locationList = list.map((i) => Location.fromJson(i)).toList();
return jsonResponse(
result: parsedJson['result'],
resultMsg: parsedJson['resultMsg'],
locations: locationList
);
}
} // jsonResponse
这里是使用上面的 classes 解析 API 数据并创建 ListView
的状态和有状态小部件
class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
_changeBackground(int index){
print("in changebackground(): ${index}"); // this works!
_listFuture[index].backgroundColor = '34bdeb'; // ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return
Container(
color: HexColor(items[index].backgroundColor),
child:
ListTile(
title: Text(items[index].companyName),
onTap: () {
print("Item at $index is ${items[index].companyName}");
_changeBackground(index);
} // onTap
)
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
helper class(取自 Whosebug 的某处)用于将 HEX 转换为整数颜色
class HexColor extends Color {
static int _getColorFromHex(String hexColor) {
hexColor = hexColor.toUpperCase().replaceAll("#", "");
if (hexColor.length == 6) {
hexColor = "FF" + hexColor;
}
return int.parse(hexColor, radix: 16);
}
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}
谢谢!
我建议的是从您的位置 class 中删除背景颜色,而不是在您的州中存储选择的位置。这样,您的位置列表不需要在选择项目时更改。我还会为您的位置项创建一个 StatelessWidget,它会设置背景颜色,具体取决于它是否被选中。所以:
// for the LocationItem widget callback
typedef void tapLocation(int index);
class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
final var selectedLocationIndices = Set<int>();
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
void _toggleLocation(int index) {
if (selectedLocationIndices.contains(index))
selectedLocationIndices.remove(index);
else
selectedLocationIndices.add(index);
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return LocationItem(
isSelected: selectedLocationIndices.contains(index),
onTap: () => setState({
_toggleLocation(index);
})
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
以及项目列表条目:
class LocationItem extends StatelessWidget {
final bool isSelected;
final Function tapLocation;
const LocationItem({@required this.isSelected, @required this.tapLocation, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: isSelected ? HexColor('34bdeb') : HexColor('fc7303'),
child: ListTile(
title: Text(items[index].companyName),
onTap: () => tapLocation() // onTap
)
);
}
}
对不起,我无法编译它,所以我希望它是正确的。但我想你明白了:让有状态小部件分别跟踪所选位置,并让位置决定在重建时如何呈现自己。
您可能必须使用 ListviewBuilder 而不是 FutureBuilder。我有一个类似的问题,我必须从 firestore 加载数据,操作它,然后才将它发送到 ListView,所以我不能使用 FutureBuilder。我基本上循环了 QuerySnapShot,在每个文档中进行了适当的更改,然后将其添加到列表对象 (List chatUsersList = List();):
String seenText = "";
chatSnapShot.forEach((doc) {
if (doc.seen) {
seenText = "not seen yet";
} else {
seenText = "seen " + doc.seenDate;
}
...
chatUsersList.add(Users(id, avatar, ..., seenText));
}
然后在 ListViewBuilder 中:
ListView.builder(
itemBuilder: (context, index) {
return
UserTile(uid, chatUsersList[index].peerId,
chatUsersList[index].avatar,..., chatUsersList[index].seenText);
},
itemCount: chatUsersList.length, ),
然后在 UserTile 中:
class UserTile extends StatelessWidget {
final String uid;
final String avatar;
...
final String seenText;
ContactTile(this.uid, this.avatar, ..., this.seenText);
@override
Widget build(BuildContext context) {
var clr = Colors.blueGrey;
if (seenText == "not seen yet") clr = Colors.red;
...
return
ListTile(
isThreeLine: true,
leading:
Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.cover,
image: new CachedNetworkImageProvider(avatar),
),
),
),
title: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: < Widget > [
Expanded(
child: Text(
name, style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, ),
overflow: TextOverflow.ellipsis,
maxLines: 1
)
),
Text(
timestamp, style: TextStyle(fontSize: 14.0, ),
overflow: TextOverflow.ellipsis,
maxLines: 1
),
],
),
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: < Widget > [
Text(sub, style: TextStyle(color: Colors.black87, fontSize: 14.0, ),
overflow: TextOverflow.ellipsis, ),
Text(**seenText**, style: TextStyle(color: **clr**, fontSize: 12.0, fontStyle: FontStyle.italic), ),
],
),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
...
});
}
我把相关代码贴在下面,但你可以根据我的伪解释来回答。
我正在使用 FutureBuilder 构建列表视图。
- 我首先使用 init() 异步 HTTP 调用 API 并将其解析为对象列表(位置)映射以表示 json 结果。
- 然后将位置列表返回到
Future<List<Location>> _listFuture
变量(这是 FutureBuilder 的未来)。 - 一旦未来 "returns" 或 "finishes" FutureBuilder 启动并使用 ListView.builder/Container/ListTile 循环遍历并构建列表。
- 在某些时候,我需要一个 onTap() 处理程序(在 ListTile 中)来更改所选列表项的背景颜色。
- 为了支持这一点,我在位置 class 中有一个 backgroundColor 成员(包含 JSON 响应),我默认为所有项目设置为“#fc7303”(假设一切都是初始状态未选中)。然后我想在 onTap() 中将所选内容的背景更改为“#34bdeb”。
- 我假设我可以调用 setState() 来触发刷新,并且新的背景颜色在重绘时将是 noticed/used。
问题是 ListView/Contrainer/ListTile 是由
驱动的Future<List<Location>>
。我可以将 "tapped" 索引传递给我的 ontap 处理程序,但我不相信我可以让我的 _changeBackground() 只更新所选索引的 backgroundColor 值并调用 setState() 因为你不能直接 access/update a那样的未来(我收到错误 ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
)
我不确定我采取的方法是否正确。在这种情况下,我想我总是可以在理论上将 "background" 颜色跟踪分离到一个新的单独列表(在未来之外)并且 track/reference 使用来自 onTap() 的对齐索引。
但是,我不确定这是否总是有效。将来,我可能需要实际更改将来返回的 values/state 。例如,想想我是否希望能够单击列表项并更新 "companyName"。在这种情况下,我将直接更改存储在未来的值。我想我可以在技术上将新名称发送到服务器并以这种方式完全刷新列表,但这似乎效率低下(如果他们决定 "cancel" 而不保存更改怎么办?)。
感谢任何帮助。谢谢!
这个class实际上保存了列表的相关数据
// Location
class Location {
// members
String locationID;
String locationName;
String companyName;
String backgroundColor = 'fc7303';
// constructor?
Location({this.locationID, this.locationName, this.companyName});
// factory?
factory Location.fromJson(Map<String, dynamic> json) {
return Location(
locationID: json['locationID'],
locationName: json['locationName'],
companyName: json['companyName'],
);
}
}
此 class 是包含 "result" (success/error) 条消息的父 json 响应。它将上面的 class 实例化为一个列表来跟踪实际的 company/location 记录
//jsonResponse
class jsonResponse{
String result;
String resultMsg;
List<Location> locations;
jsonResponse({this.result, this.resultMsg, this.locations});
factory jsonResponse.fromJson(Map<String, dynamic> parsedJson){
var list = parsedJson['resultSet'] as List;
List<Location> locationList = list.map((i) => Location.fromJson(i)).toList();
return jsonResponse(
result: parsedJson['result'],
resultMsg: parsedJson['resultMsg'],
locations: locationList
);
}
} // jsonResponse
这里是使用上面的 classes 解析 API 数据并创建 ListView
的状态和有状态小部件class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
_changeBackground(int index){
print("in changebackground(): ${index}"); // this works!
_listFuture[index].backgroundColor = '34bdeb'; // ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return
Container(
color: HexColor(items[index].backgroundColor),
child:
ListTile(
title: Text(items[index].companyName),
onTap: () {
print("Item at $index is ${items[index].companyName}");
_changeBackground(index);
} // onTap
)
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
helper class(取自 Whosebug 的某处)用于将 HEX 转换为整数颜色
class HexColor extends Color {
static int _getColorFromHex(String hexColor) {
hexColor = hexColor.toUpperCase().replaceAll("#", "");
if (hexColor.length == 6) {
hexColor = "FF" + hexColor;
}
return int.parse(hexColor, radix: 16);
}
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}
谢谢!
我建议的是从您的位置 class 中删除背景颜色,而不是在您的州中存储选择的位置。这样,您的位置列表不需要在选择项目时更改。我还会为您的位置项创建一个 StatelessWidget,它会设置背景颜色,具体取决于它是否被选中。所以:
// for the LocationItem widget callback
typedef void tapLocation(int index);
class locationsApiState extends State<locationsApiWidget> {
// list to track AJAX results
Future<List<Location>> _listFuture;
final var selectedLocationIndices = Set<int>();
// init - set initial values
@override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
Future<List<Location>> updateAndGetList() async {
var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
if (response.statusCode == 200) {
var r1 = json.decode(response.body);
jsonResponse r = new jsonResponse.fromJson(r1);
return r.locations;
} else {
throw Exception('Failed to load internet');
}
}
void _toggleLocation(int index) {
if (selectedLocationIndices.contains(index))
selectedLocationIndices.remove(index);
else
selectedLocationIndices.add(index);
}
// build() method
@override
Widget build(BuildContext context) {
return new FutureBuilder<List<Location>>(
future: _listFuture,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data;
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
//Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return LocationItem(
isSelected: selectedLocationIndices.contains(index),
onTap: () => setState({
_toggleLocation(index);
})
);
},
),
onRefresh: () {
// implement later
return;
} // refreshList,
),
);
}// else
} // builder
); // FutureBuilder
} // build
} // locationsApiState class
class locationsApiWidget extends StatefulWidget {
@override
locationsApiState createState() => locationsApiState();
}
以及项目列表条目:
class LocationItem extends StatelessWidget {
final bool isSelected;
final Function tapLocation;
const LocationItem({@required this.isSelected, @required this.tapLocation, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: isSelected ? HexColor('34bdeb') : HexColor('fc7303'),
child: ListTile(
title: Text(items[index].companyName),
onTap: () => tapLocation() // onTap
)
);
}
}
对不起,我无法编译它,所以我希望它是正确的。但我想你明白了:让有状态小部件分别跟踪所选位置,并让位置决定在重建时如何呈现自己。
您可能必须使用 ListviewBuilder 而不是 FutureBuilder。我有一个类似的问题,我必须从 firestore 加载数据,操作它,然后才将它发送到 ListView,所以我不能使用 FutureBuilder。我基本上循环了 QuerySnapShot,在每个文档中进行了适当的更改,然后将其添加到列表对象 (List chatUsersList = List();):
String seenText = "";
chatSnapShot.forEach((doc) {
if (doc.seen) {
seenText = "not seen yet";
} else {
seenText = "seen " + doc.seenDate;
}
...
chatUsersList.add(Users(id, avatar, ..., seenText));
}
然后在 ListViewBuilder 中:
ListView.builder(
itemBuilder: (context, index) {
return
UserTile(uid, chatUsersList[index].peerId,
chatUsersList[index].avatar,..., chatUsersList[index].seenText);
},
itemCount: chatUsersList.length, ),
然后在 UserTile 中:
class UserTile extends StatelessWidget {
final String uid;
final String avatar;
...
final String seenText;
ContactTile(this.uid, this.avatar, ..., this.seenText);
@override
Widget build(BuildContext context) {
var clr = Colors.blueGrey;
if (seenText == "not seen yet") clr = Colors.red;
...
return
ListTile(
isThreeLine: true,
leading:
Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.cover,
image: new CachedNetworkImageProvider(avatar),
),
),
),
title: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: < Widget > [
Expanded(
child: Text(
name, style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, ),
overflow: TextOverflow.ellipsis,
maxLines: 1
)
),
Text(
timestamp, style: TextStyle(fontSize: 14.0, ),
overflow: TextOverflow.ellipsis,
maxLines: 1
),
],
),
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: < Widget > [
Text(sub, style: TextStyle(color: Colors.black87, fontSize: 14.0, ),
overflow: TextOverflow.ellipsis, ),
Text(**seenText**, style: TextStyle(color: **clr**, fontSize: 12.0, fontStyle: FontStyle.italic), ),
],
),
trailing: Icon(Icons.keyboard_arrow_right),
onTap: () {
...
});
}