flutterlistviewprovider

Flutter ListView not updating despite data list has been updated unless I scrolled all the way to the end


I'm trying to implement this posts-filter functions using Provider in Flutter, the posts has title, timestamp, pfp, and number of likes/comments, my problem is let's say if the user sorted to Oldest what happen is that the value of likes/comments sorted correctly but the title/pfp/timestamp are not and remain static in their current position but until I scrolled all the way down and up the ListView updated and the bug fixed by itself, why?

The filter class:

class SearchPostsFilter with SearchProviderService {

  final formatTimestamp = FormatDate();

  void filterPostsToBest() {

    final sortedVents = searchPostsProvider.vents
      .toList()
      ..sort((a, b) => a.totalLikes.compareTo(b.totalLikes));

    searchPostsProvider.setVents(sortedVents);

  }

  void filterPostsToLatest() {

    final sortedVents = searchPostsProvider.vents
      .toList()
      ..sort((a, b) => formatTimestamp.parseFormattedTimestamp(b.postTimestamp)
        .compareTo(formatTimestamp.parseFormattedTimestamp(a.postTimestamp)));

    searchPostsProvider.setVents(sortedVents);

  }

  void filterPostsToOldest() {

    final sortedVents = searchPostsProvider.vents
      .toList()
      ..sort((a, b) => formatTimestamp.parseFormattedTimestamp(a.postTimestamp)
        .compareTo(formatTimestamp.parseFormattedTimestamp(b.postTimestamp)));

    searchPostsProvider.setVents(sortedVents);

  }

  void filterToControversial() {

    final sortedVents = searchPostsProvider.vents
      .where((post) => post.totalComments >= post.totalLikes)
      .toList();

    searchPostsProvider.setVents(sortedVents);    

  }

}

The provider/data classes where the post info is stored:

class SearchVents {

  String title;
  String creator;
  String postTimestamp;

  Uint8List profilePic;

  int totalLikes;
  int totalComments;

  bool isPostLiked;
  bool isPostSaved;

  SearchVents({
    required this.title,
    required this.creator,
    required this.postTimestamp,
    required this.profilePic,
    this.totalLikes = 0,
    this.totalComments = 0,
    this.isPostLiked = false,
    this.isPostSaved = false
  });

}

class SearchPostsProvider extends ChangeNotifier {

  List<SearchVents> _vents = [];

  List<SearchVents> get vents => _vents;

  void setVents(List<SearchVents> vents) {
    _vents = vents;
    notifyListeners();
  }

}

The posts ListView:

class SearchPostsListView extends StatefulWidget {

  const SearchPostsListView({super.key});

  @override
  State<SearchPostsListView> createState() => _SearchPostsListViewState();

}

class _SearchPostsListViewState extends State<SearchPostsListView> {

  final sortOptionsNotifier = ValueNotifier<String>('Best');

  final searchPostsFilter = SearchPostsFilter();

  Widget _buildVentPreview(SearchVents ventData) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8.5),
      child: DefaultVentPreviewer(
        title: ventData.title,
        bodyText: '',
        creator: ventData.creator,
        postTimestamp: ventData.postTimestamp,
        totalLikes: ventData.totalLikes,
        totalComments: ventData.totalComments,
        pfpData: ventData.profilePic,
        useV2ActionButtons: true,
      ),
    );
  }

  void _sortOptionsOnPressed({required String filter}) {
    
    switch (filter) {
      case == 'Best':
        searchPostsFilter.filterPostsToBest();
        break;
      case == 'Latest':
        searchPostsFilter.filterPostsToLatest();
        break;
      case == 'Oldest':
        searchPostsFilter.filterPostsToOldest();
        break;
      case == 'Controversial':
        searchPostsFilter.filterToControversial();
        break;
    }

    sortOptionsNotifier.value = filter;

    Navigator.pop(context);

  }


  Widget _buildFilterButtons({
    required ValueListenable notifier, 
    required VoidCallback onPressed
  }) {
    return SizedBox(
      height: 35,
      child: InkWellEffect(
        onPressed: onPressed,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
      
            const SizedBox(width: 8),

            const Icon(CupertinoIcons.chevron_down, color: ThemeColor.thirdWhite, size: 15),
    
            const SizedBox(width: 8),
    
            ValueListenableBuilder(
              valueListenable: notifier,
              builder: (_, filterText, __) {
                return Text(
                  filterText,
                  style: GoogleFonts.inter(
                    color: ThemeColor.thirdWhite,
                    fontWeight: FontWeight.w800,
                    fontSize: 13
                  )
                );
              },
            ),
  
            const SizedBox(width: 8),
    
          ],
        ),
      ),
    );
  }

  Widget _buildListView(List<SearchVents> ventDataList) {
    return DynamicHeightGridView(
      physics: const AlwaysScrollableScrollPhysics(
        parent: BouncingScrollPhysics(),
      ),
      crossAxisCount: 1,
      itemCount: ventDataList.length + 1,
      builder: (_, index) {

        if (index == 0) {
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
            
              const SizedBox(height: 12),

              Row(
                children: [

                  _buildFilterButtons(
                    notifier: sortOptionsNotifier,
                    onPressed: () {
                      BottomsheetSearchFilter().buildSortOptionsBottomsheet(
                        context: context, 
                        currentFilter: sortOptionsNotifier.value,
                        bestOnPressed: () => _sortOptionsOnPressed(filter: 'Best'), 
                        latestOnPressed: () => _sortOptionsOnPressed(filter: 'Latest'),
                        oldestOnPressed: () => _sortOptionsOnPressed(filter: 'Oldest'),
                        controversialOnPressed: () => _sortOptionsOnPressed(filter: 'Controversial'),
                      );
                    }
                  ),

                ],
              ),

              const SizedBox(height: 4),

            ],
          );
        }

        final reversedVentIndex = ventDataList.length - index;
        
        if (reversedVentIndex >= 0) {
          final vents = ventDataList[reversedVentIndex];
          return _buildVentPreview(vents);
        }

        return const SizedBox.shrink();

      },
    );
  }

  @override
  void dispose() {
    sortOptionsNotifier.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    searchPostsFilter.filterPostsToBest();
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<SearchPostsProvider>(
      builder: (_, ventData, __) {

        final ventDataList = ventData.vents;

        return _buildListView(ventDataList);

      }
    );
  }

}

Now keep in mind even when ListView is not correctly sorted, somehow when I printed the data list it shows exactly how it should be sorted but it does not reflect on the ListView.

  void filterPostsToOldest() {

    final sortedVents = searchPostsProvider.vents
      .toList()
      ..sort((a, b) => formatTimestamp.parseFormattedTimestamp(a.postTimestamp)
        .compareTo(formatTimestamp.parseFormattedTimestamp(b.postTimestamp)));

    for(var post in sortedVents) {
      print('${post.title} / ${post.postTimestamp}');
    }

    searchPostsProvider.setVents(sortedVents);

  }

So I tried to use KeyedSubtree on the ListView as suggested by ChatGPT but it does not work and nothing changes

return KeyedSubtree(
        key: ValueKey('${vents.title}/${vents.creator}'),
              child: _buildVentPreview(vents),
       );

TLDR; ListView doesn't correctly match with list data.


Solution

  • You don't need the keyed subtree, you can just add a UniqueKey to the Column you return in your builder. A key will identify each element in your listview so when you're rearranging them, they're correctly identified, so you have to add the key at the topmost widget in the subtree, in this case for each view card, the topmost element would be the Column, so just give it a UniqueKey(), that should sort it.

    If you wanna understand it more, you can check this video