1

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);
}
1
  • 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? Commented Mar 3, 2021 at 13:05

3 Answers 3

5

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
             });
          });
        })
});
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you very much, that worked. I had to update my data again within the inner setState(), but that did the trick.
I don't think the first setState is needed anymore.
1

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

Comments

1

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

2 Comments

In general, an empty setState is a code smell.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.