Dismissible 和 FutureBuilder 不能一起工作
Dismissible and FutureBuilder do not work together
这是一个复杂的问题,但我会尽力解释。
我的项目使用 sqflite 数据库。此特定页面 returns 根据数据库中的数据列出了可关闭的小部件。这是我读取数据的方式:
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
Future<List<Task>> allTasks;
@override
void initState() {
allTasks = dbProvider.getAllTasks();
super.initState();
}
void update(){
setState(() {
allTasks = dbProvider.getAllTasks();
});
}
//build
}
TaskList 小部件 returns 带有 FutureBuilder 的页面,它使用数据库中的数据构建 ListView.builder。 ListView 构建 Dismissible 小部件。关闭 Dismissible 小部件会更新数据库中的一行并再次读取数据以更新列表。
TaskListState 的构建方法
@override
Widget build(BuildContext context) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets such as a title for the list
),
FutureBuilder(
future: allTasks,
builder: (context, snapshot){
if(snapshot.hasError){
return Text("Data has error.");
} else if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(),
);
} else {
return pendingList(Task.filterByDone(false, Task.filterByDate(Datetime.now, snapshot.data))); //filters the data to match current day
}
},
),
//other widgets
],
);
}
pendingList
Widget pendingList(List<Task> tasks){
//some code to return a Text widget if "tasks" is empty
return ListView.separated(
separatorBuilder: (context, index){
return Divider(height: 2.0);
},
itemCount: tasks.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index){
return Dismissible(
//dismissible backgrounds, other non-required parameters
key: Key(UniqueKey().toString()),
onDismissed: (direction) async {
Task toRemove = tasks[index]; //save the dismissed task for the upcoming operations
int removeIndex = tasks.indexWhere((task) => task.id == toRemove.id);
tasks.removeAt(removeIndex); //remove the dismissed task
if(direction == DismissDirection.endToStart) {
rateTask(toRemove).then((value) => update()); //rateTask is a function that updates the task, it is removed from the list
}
if(direction == DismissDirection.startToEnd) {
dbProvider.update(/*code to update selected task*/).then((value) => update());
}
},
child: ListTile(
//ListTile details
),
);
},
);
}
这里是问题所在(可能是一个错误的解释我还是个新手):
关闭小部件实质上是将其从列表中删除。用户关闭任务后,任务将从列表中删除 "visually" 并调用 update()
方法,该方法调用 setState()
。调用 setState()
会导致 FutureBuilder 再次构建,但是 dbProvider.getAllTasks()
调用在 FutureBuilder 再次构建时尚未完成。因此,FutureBuilder 传递了旧快照,这导致 ListView 使用刚刚被取消的任务再次构建。这会导致被解雇的 ListTile 在被解雇后立即出现,这看起来令人毛骨悚然且错误。
我不知道如何解决这个问题。任何帮助将不胜感激。
通过不使用 FutureBuilder 并在查询完成后调用 setState 找到了解决此问题的方法。
状态现在包含一个 List<Task>
,而不是 Future<List<Task>>
,它被声明为 null。
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
DateTime now = DateTime.now();
List<Task> todayTasks;
//build
}
update()
函数修改如下
void update() async {
Future<List<Task>> futureTasks = dbProvider.getByDate(now); //first make the query
futureTasks.then((data){
List<Task> tasks = new List<Task>();
for(int i = 0; i < data.length; i++) {
print(data[i].name);
tasks.add(data[i]);
}
setState(() {
todayTasks = tasks; //then setState and rebuild the widget
});
});
}
这样我就不会在 future 完成之前重建小部件,这是我遇到的问题。
我完全删除了 FutureBuilder,Listview.builder 只是根据状态中存储的列表构建。
@override
Widget build(BuildContext context) {
if(todayTasks == null) {
update();
return Center(
child: CircularProgressIndicator(),
);
} //make a query if the list has not yet been initialized
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets
pendingList(Task.filterByDone(false, todayTasks)),
],
);
}
这种方法完全解决了我的问题,我认为它比使用 FutureBuilder 更好,以防 Future 必须 在再次构建小部件之前完成。
我遇到了完全相同的问题,我使用的是与 Futures
一起使用的 sqflite,所以我最终将 FutureBuilder
与 Dismissible
一起用于我的 ListView
。被取消的列表项将被删除,然后重新出现一个框架,然后再次消失。我遇到了这个问题:
https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc
建议从快照数据本身中删除列表项:
return FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Dismissible(
onDismissed: (direction) {
snapshot.data.removeAt(i); // to avoid weird future builder issue with dismissible
Provider.of<FoldersProvider>(context, listen: false).deleteFolder(folder.id);
},
background: Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 1,
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(15.0, 0.0, 0.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: UniqueKey(),
direction: DismissDirection.startToEnd,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.folder,
color: Theme.of(context).accentColor,
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
),
),
);
},
);
},
);
瞧瞧,它奏效了!对代码的最小改动:)
这是一个复杂的问题,但我会尽力解释。
我的项目使用 sqflite 数据库。此特定页面 returns 根据数据库中的数据列出了可关闭的小部件。这是我读取数据的方式:
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
Future<List<Task>> allTasks;
@override
void initState() {
allTasks = dbProvider.getAllTasks();
super.initState();
}
void update(){
setState(() {
allTasks = dbProvider.getAllTasks();
});
}
//build
}
TaskList 小部件 returns 带有 FutureBuilder 的页面,它使用数据库中的数据构建 ListView.builder。 ListView 构建 Dismissible 小部件。关闭 Dismissible 小部件会更新数据库中的一行并再次读取数据以更新列表。
TaskListState 的构建方法
@override
Widget build(BuildContext context) {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets such as a title for the list
),
FutureBuilder(
future: allTasks,
builder: (context, snapshot){
if(snapshot.hasError){
return Text("Data has error.");
} else if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(),
);
} else {
return pendingList(Task.filterByDone(false, Task.filterByDate(Datetime.now, snapshot.data))); //filters the data to match current day
}
},
),
//other widgets
],
);
}
pendingList
Widget pendingList(List<Task> tasks){
//some code to return a Text widget if "tasks" is empty
return ListView.separated(
separatorBuilder: (context, index){
return Divider(height: 2.0);
},
itemCount: tasks.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index){
return Dismissible(
//dismissible backgrounds, other non-required parameters
key: Key(UniqueKey().toString()),
onDismissed: (direction) async {
Task toRemove = tasks[index]; //save the dismissed task for the upcoming operations
int removeIndex = tasks.indexWhere((task) => task.id == toRemove.id);
tasks.removeAt(removeIndex); //remove the dismissed task
if(direction == DismissDirection.endToStart) {
rateTask(toRemove).then((value) => update()); //rateTask is a function that updates the task, it is removed from the list
}
if(direction == DismissDirection.startToEnd) {
dbProvider.update(/*code to update selected task*/).then((value) => update());
}
},
child: ListTile(
//ListTile details
),
);
},
);
}
这里是问题所在(可能是一个错误的解释我还是个新手):
关闭小部件实质上是将其从列表中删除。用户关闭任务后,任务将从列表中删除 "visually" 并调用 update()
方法,该方法调用 setState()
。调用 setState()
会导致 FutureBuilder 再次构建,但是 dbProvider.getAllTasks()
调用在 FutureBuilder 再次构建时尚未完成。因此,FutureBuilder 传递了旧快照,这导致 ListView 使用刚刚被取消的任务再次构建。这会导致被解雇的 ListTile 在被解雇后立即出现,这看起来令人毛骨悚然且错误。
我不知道如何解决这个问题。任何帮助将不胜感激。
通过不使用 FutureBuilder 并在查询完成后调用 setState 找到了解决此问题的方法。
状态现在包含一个 List<Task>
,而不是 Future<List<Task>>
,它被声明为 null。
class TaskListState extends State<TaskList> {
DBProvider dbProvider = new DBProvider();
DateTime now = DateTime.now();
List<Task> todayTasks;
//build
}
update()
函数修改如下
void update() async {
Future<List<Task>> futureTasks = dbProvider.getByDate(now); //first make the query
futureTasks.then((data){
List<Task> tasks = new List<Task>();
for(int i = 0; i < data.length; i++) {
print(data[i].name);
tasks.add(data[i]);
}
setState(() {
todayTasks = tasks; //then setState and rebuild the widget
});
});
}
这样我就不会在 future 完成之前重建小部件,这是我遇到的问题。
我完全删除了 FutureBuilder,Listview.builder 只是根据状态中存储的列表构建。
@override
Widget build(BuildContext context) {
if(todayTasks == null) {
update();
return Center(
child: CircularProgressIndicator(),
);
} //make a query if the list has not yet been initialized
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
//other widgets
pendingList(Task.filterByDone(false, todayTasks)),
],
);
}
这种方法完全解决了我的问题,我认为它比使用 FutureBuilder 更好,以防 Future 必须 在再次构建小部件之前完成。
我遇到了完全相同的问题,我使用的是与 Futures
一起使用的 sqflite,所以我最终将 FutureBuilder
与 Dismissible
一起用于我的 ListView
。被取消的列表项将被删除,然后重新出现一个框架,然后再次消失。我遇到了这个问题:
https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc
建议从快照数据本身中删除列表项:
return FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Dismissible(
onDismissed: (direction) {
snapshot.data.removeAt(i); // to avoid weird future builder issue with dismissible
Provider.of<FoldersProvider>(context, listen: false).deleteFolder(folder.id);
},
background: Card(
margin: EdgeInsets.symmetric(vertical: 8),
elevation: 1,
child: Container(
alignment: AlignmentDirectional.centerStart,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(15.0, 0.0, 0.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: UniqueKey(),
direction: DismissDirection.startToEnd,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.folder,
color: Theme.of(context).accentColor,
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
),
),
);
},
);
},
);
瞧瞧,它奏效了!对代码的最小改动:)