2
votes

It is kind of a complex problem but I'll do my best to explain it.

My project utilizes a sqflite database. This particular page returns a list of Dismissible widgets according to the data in the database. This is how I read the data:

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
}

The TaskList widget returns a page with a FutureBuilder, which builds a ListView.builder with the data from the database. The ListView builds Dismissible widgets. Dismissing the Dismissible widgets updates a row in the database and reads the data again to update the list.

build method for 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
      ],
    );
  }

The 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
          ),
        );
      },
    );
  }

Here is the problem (might be a wrong interpretation I'm still kind of new):

Dismissing a widget essentially removes it from the list. After the user dismisses a task, the task is "visually" removed from the list and the update() method is called, which calls setState(). Calling setState() causes the FutureBuilder to build again, but the dbProvider.getAllTasks() call is not completed by the time the FutureBuilder builds again. Therefore, the FutureBuilder passes the old snapshot, which causes the ListView to build again with the Task that just was dismissed. This causes the dismissed ListTile to appear momentarily after being dismissed, which looks creepy and wrong.

I have no idea how to fix this. Any help would be appreciated.

2

2 Answers

6
votes

I was having the exact same issue, I was using sqflite which works with Futures so I ended up using the FutureBuilder alongside Dismissible for my ListView. The dismissed list item would remove then reappear for a frame then disappear again. I came across this question :

https://groups.google.com/forum/#!topic/flutter-dev/pC48MMVKJGc

which suggests removing the list item from the snapshot data itself:

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: () {},                  
                ),
              ),
            );
          },
        );
      },
    );

and low and behold, it worked! Minimal changes to the code :)

1
votes

Found a workaround for this by not using FutureBuilder and calling setState after the query is completed.

Instead of Future<List<Task>>, the state now contains a List<Task> which is declared as null.

class TaskListState extends State<TaskList> {
  DBProvider dbProvider = new DBProvider();
  DateTime now = DateTime.now();
  List<Task> todayTasks;
  //build
}

The update() function was changed as follows

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
      });
    });
  }

This way I the widget does not rebuild before the future is completed, which was the problem I had.

I removed the FutureBuilder completely, the Listview.builder just builds accordingly to the List stored in state.

@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)),
      ],
    );
  }

This approach completely solved my problem, and I think its better than using a FutureBuilder in case the Future must be completed before the widget builds again.