Flutter setState() after completion of async function

2,581

Solution 1

Try this

Change _remove to future void

Future<void> _remove(int id) async {
    DatabaseHelper helper = DatabaseHelper.instance;
    await helper.deleteTransaction(id);
}

then change your setstate

setState(() {
          _remove(transactionsReversed[index].id).then((_) {
             setState(() {
               //setstate
             });
          });
        })
});

Solution 2

Your issue seems to be related to the fact that you are calling setState immediately as delete is called, which means that the view rebuild is triggered straight away, before helper.deleteTransaction(id); is called. I would suggest that you only call setState after the future is returned from your async _remove method, like this:

Add the Future<void> type to your _remove method:

Future<void> _remove(int id) async {
  DatabaseHelper helper = DatabaseHelper.instance;
  await helper.deleteTransaction(id);
}

Call your setState only once you receive the future as you can see below:

delete: (){
  print('Running delete function on ${transactionsReversed[index].id}');
  setState(() {
  _remove(transactionsReversed[index].id).then((value) => setState((){}));
  });
}

Solution 3

As mentioned in other answers, the problem was due to setState running before the async metod _remove completion.

Since _remove is a private method inside your Widget class, maybe it could take the setState in charge.

Your _removebecomes:

Future<void> _remove(int id) async {
  await DatabaseHelper.instance.deleteTransaction(id);
  setState((){});
}

And you can simplify your delete callback to:

ListView.builder(
  [...]
  itemBuilder: (context, index) {
    return CustomListItem(
      [...]
      delete: () => _remove(transactionsReversed[index].id),
    );
  }
),
Share:
2,581
Matze
Author by

Matze

Updated on December 28, 2022

Comments

  • Matze
    Matze over 1 year

    I have created a ListView.builder of custom Widgets based on entries in a database. Now I am trying to add a delete function. When I click on the button, the entry gets removed from the database accordingly. However, the ListView does not update despite setState() being called. I suspect that this might be due to the delete method being async, how can I have the app first delete the entry and then refresh the ListView?

    The code for my ListView.builder is:

    ListView.builder(
      padding: const EdgeInsets.all(8.0),
      itemExtent: 106.0,
      itemCount: snapshot.data.length,
      itemBuilder: (context, index) {
        return CustomListItem(
          name: transactionsReversed[index].name,
          category: transactionsReversed[index].category,
          sign: transactionsReversed[index].sign,
          amount: transactionsReversed[index].amount,
          id: transactionsReversed[index].id,
          delete: (){
            print('Running delete function on ${transactionsReversed[index].id}');
            setState(() {
              _remove(transactionsReversed[index].id);
            });
          }
        );
      }
    ),
    

    The custom list item looks like this:

    class CustomListItem extends StatelessWidget {
      const CustomListItem({
        this.name, this.category, this.sign, this.amount, this.id, this.delete
      });
    
      final String name;
      final String category;
      final String sign;
      final double amount;
      final int id;
      final Function delete;
    
      @override
      Widget build(BuildContext context) {
    
        const double radius = 15.0;
    
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 5.0),
          child: InkWell(
            onTap: () {
              Navigator.pushNamed(context, '/category_viewer', arguments: {
                'category': category
              });
            },
            child: Container(
              color: Colors.transparent,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.black,
                    borderRadius: new BorderRadius.only(
                      topLeft: const Radius.circular(radius),
                      topRight: const Radius.circular(radius),
                      bottomLeft: const Radius.circular(radius),
                      bottomRight: const Radius.circular(radius),
                    ),
                ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Expanded(
                        flex: 1,
                        child: IconButton(
                          onPressed: delete,
                          icon: Icon(
                              Icons.delete_outlined,
                              color: Colors.red,
                              size: 40,
                          ),
                          color: Colors.red
                        ),
                      ),
                      Expanded(
                        flex: 3,
                        child: Padding(
                          padding: const EdgeInsets.fromLTRB(5.0, 0.0, 0.0, 0.0),
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              Text(
                                name,
                                style: const TextStyle(
                                    fontWeight: FontWeight.w500,
                                    fontSize: 20.0,
                                    color: Colors.white
                                ),
                              ),
                              const Padding(padding: EdgeInsets.symmetric(vertical: 2.0)),
                              Text(
                                category,
                                style: const TextStyle(
                                    fontSize: 14.0,
                                    color: Colors.white
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
    
                      Padding(
                        padding: const EdgeInsets.fromLTRB(0, 0, 50, 0),
                        child: Text(
                          '$sign${amount.toStringAsFixed(2)}€',
                          style: TextStyle(
                              fontSize: 25.0,
                              color: sign == '+' ? Colors.green : Colors.red
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
      }
    }
    

    Finally, the remove method is simply:

    _remove(int id) async {
        DatabaseHelper helper = DatabaseHelper.instance;
        await helper.deleteTransaction(id);
    }
    
    • aoiTenshi
      aoiTenshi about 3 years
      I am not sure but maybe you can initialize a bool. Then, if bool is true you refresh the listview. It starts with false, when you delete the entry you may setState your bool to true and then refresh works?
  • Matze
    Matze about 3 years
    Thank you very much, that worked. I had to update my data again within the inner setState(), but that did the trick.
  • Thierry
    Thierry about 3 years
    I don't think the first setState is needed anymore.
  • Randal Schwartz
    Randal Schwartz about 3 years
    In general, an empty setState is a code smell.
  • Thierry
    Thierry about 3 years
    Fully agree with that point. I tend to go even one step further and consider that having a setState is a code smell. But with the information provided in the question, I'm not sure what State Management architecture (if any) is in place.