Flutter ListView doesn't refresh UI with setState(), although itemCount and attached list update correctly
Asked Answered
A

2

10

I see a lot of people with very similar problems, but nothing I try works.

CONTEXT

I have a list of favorited ideas. Whenever I click in a button inside the ideaItem, it should get removed from the list.

PROBLEM

When I delete any ideaItem, the last one from the screen gets always removed, instead of the one I clicked on. My FavoriteIdeasListView seems to update correctly the items count, this means that the attached List works, but the UI is not redrawing the ideaItems.

WHAT I HAVE TRIED

  • At the beginning I had the delete functionality directly on ideaItem, I read I should do a VoidCallback and handle deletion from the List itself, so it would notice the change. It didn't work

  • I also tried with a Stream Builder, so the stream would notify the ListView to refresh. Also it didn't work

  • I try calling SetState all the time and it's not reloading, it only builds the list at the beginning on the initialState.


    class FavoritesList extends StatefulWidget {
      FavoritesList({Key key}) : super(key: key);

      @override
      _FavoritesListState createState() => _FavoritesListState();
    }

    class _FavoritesListState extends State<FavoritesList> {

      List<Idea> _favorites = [];

      @override
      void initState() {
        super.initState();
        favoritesInitialState();
      }

      Future <void> favoritesInitialState() async {
        List<Idea> ideas = await IdeasDB.db.ideas();
        setState(() {
          _favorites = ideas;
        });
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text('Favorites')),
            body: Center(
              child: ListView.builder(
                itemCount: _favorites.length,
                itemBuilder: (context, index) {
                  final idea = _favorites[index];
                  return IdeaItem(
                    idea: idea,
                    onFavoriteToggle: () => deleteFromFavorites(idea),
                  );
                },
              ),
            )
        );
      }

      void deleteFromFavorites(Idea idea) async {
        await IdeasDB.db.deleteIdea(idea.url);
        List<Idea> newIdeas = await IdeasDB.db.ideas();
        setState(() {
          this._favorites = newIdeas;
        });
      }
    }
Allochthonous answered 18/11, 2019 at 17:22 Comment(0)
D
8

I can see only two possible options for why this happens.

First one is obvious, your IdeasDB.db.deleteIdea(idea.url); doesn't delete correct idea from DB;

The second one is less obvious, it's because your Element Tree couldn't recognize what's a widget you are trying to delete. It happens only with Stateful Widgets as your IdeaItem could be.

Solution then is to use key attribute for your IdeaItem widget like this:

ListView.builder(
  itemCount: _favorites.length,
  itemBuilder: (context, index) {
    final idea = _favorites[index];
    return IdeaItem(
      key: ValueKey(idea.url) // or UniqueKey()
      idea: idea,
      onFavoriteToggle: () => deleteFromFavorites(idea),
    );
  },
),

and in your IdeaItem widget you have to pass that key to your parent Stateful widget like in this example:

class IdeaItem extends StatefulWidget {
  final Idea idea;
  Function onFavoriteToggle;

  IdeaItem({Key key, this.idea, this.onFavoriteToggle}) : super(key: key);

  @override
  _IdeaItemState createState() => _IdeaItemState();
}
Dobbins answered 18/11, 2019 at 18:41 Comment(1)
It works! the solution was to add the key because indeed my IdeaItem was also Stateful! Amazing!Allochthonous
P
3

Try to change

onFavoriteToggle: () => deleteFromFavorites(index),

and then

  void deleteFromFavorites( int index) async {
    await IdeasDB.db.deleteIdea(idea.url);
    setState(() {
      this._favorites.removeAt(index);
    });
  }

See if it works. If yes, there is probably error on deleteIdea().

Also, you know which item should be deleted, so you don't need to await to reload the List from your db. await IdeasDB.db.ideas();

If you want to be sure you can wrap your code with Try catch

  void deleteFromFavorites(int index) async {
    try {
      await IdeasDB.db.deleteIdea(idea.url);
      setState(() {
        this._favorites.removeAt(index);
      });
    } catch (_) {
      print('ERROR');
    }
  }
Poleyn answered 18/11, 2019 at 19:0 Comment(1)
The solution was to pass the 'key' parameter. I upvote because the optimise solution removing unnecessary waiting! Thanks!Allochthonous

© 2022 - 2024 — McMap. All rights reserved.