Flutter appending fetched data to a listview inside a future builder

207

What you need is to hold onto the state in some Listenable, such as ValueNotifier and use ValueListenableBuilder to build your ListView. I put together this demo to show you what I mean:

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: const HomePage(),
  ));
}

@immutable
class Person {
  final String id;
  Person() : id = const Uuid().v4();
}

class DataController extends ValueNotifier<Iterable<Person>> {
  DataController() : super([]) {
    addMoreValues();
  }

  void addMoreValues() {
    value = value.followedBy(
      Iterable.generate(
        30,
        (_) => Person(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final ScrollController _controller;
  final _generator = DataController();

  @override
  void initState() {
    super.initState();
    _controller = ScrollController();
    _controller.addListener(() {
      if (_controller.position.atEdge && _controller.position.pixels != 0.0) {
        _generator.addMoreValues();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: ValueListenableBuilder(
        valueListenable: _generator,
        builder: (context, value, child) {
          final persons = value as Iterable<Person>;
          return ListView.builder(
            controller: _controller,
            itemCount: persons.length,
            itemBuilder: (context, index) {
              final person = persons.elementAt(index);
              return ListTile(
                title: Text(person.id),
              );
            },
          );
        },
      ),
    );
  }
}
Share:
207
Midas King
Author by

Midas King

Updated on January 03, 2023

Comments

  • Midas King
    Midas King over 1 year

    I'd like to ask when using the FutureBuilder to display fetched data from a remote server in a ListView. I check if the bottom of the ListView was reached using ScrollController. Everything is working well until I try to load new data and append them to the existing ListView I fetch the data add them to my Array and the in setState((){}) I update the list for the FutureBuilder this is obviously the wrong approach since then the whole FutureBuilder is rebuilt and so is the ListView. The changes however do appear all the new items are in the list as intended however it slows performance not significantly since ListView is not keeping tiles out of view active but it has a small impact on performance, but the main issue is that since ListView gets rebuilt, I'm thrown as a user to the start of this list that's because the ListView got rebuilt. Now what I would like to achieve is that the ListView doesn't get rebuilt every time I get new data. Here is the code of the whole StateFulWidget

    import 'package:connectivity_plus/connectivity_plus.dart';
    import 'package:flutter/material.dart';
    
    import '../widgets/rss_card.dart';
    import '../extensions/colors.dart';
    import '../extensions/rss.dart';
    import '../main.dart';
    import '../models/rss.dart';
    
    class RssListView extends StatefulWidget {
      final String? channel;
    
      const RssListView.fromChannel(this.channel, {Key? key}) : super(key: key);
    
      @override
      State<RssListView> createState() => _RssListViewState();
    }
    
    class _RssListViewState extends State<RssListView>
        with AutomaticKeepAliveClientMixin {
      late RssListModel _rssListModel;
      double _offset = 0.0;
      final double _limit = 5.0;
      Future<List<RssItemModel>?>? _rssFuture;
      final ScrollController _scrollController = ScrollController();
    
      Map<String, Object> _args({double? newOffset}) => {
            'offset': newOffset ?? _offset,
            'limit': _limit,
          };
    
      Future<bool> isConnected() async {
        var conn = await Connectivity().checkConnectivity();
        return (conn == ConnectivityResult.mobile ||
                conn == ConnectivityResult.wifi ||
                conn == ConnectivityResult.ethernet)
            ? true
            : false;
      }
    
      Future<void> _pullRefresh() async {
        _rssListModel.refresh(_args(
          newOffset: 0,
        ));
        List<RssItemModel>? refreshedRssItems = await _rssListModel.fetchData();
        setState(() {
          _rssFuture = Future.value(refreshedRssItems);
        });
      }
    
      Future<List<RssItemModel>?> get initialize async {
        await _rssListModel.initializationDone;
        return _rssListModel.Items;
      }
    
      void _loadMore() async {
        List<RssItemModel>? moreItems = await _rssListModel
            .loadMoreWithArgs(_args(newOffset: _offset += _limit));
        setState(() {
          _rssFuture = Future.value(moreItems);
        });
      }
    
      void _showSnackBarWithDelay({int? milliseconds}) {
        Future.delayed(
          Duration(milliseconds: milliseconds ?? 200),
          () {
            ScaffoldMessenger.of(context).showSnackBar(getDefaultSnackBar(
              message: 'No Internet Connection',
            ));
          },
        );
      }
    
      void _addScrollControllerListener() {
        _scrollController.addListener(() {
          if (_scrollController.position.pixels ==
              (_scrollController.position.maxScrollExtent)) _loadMore();
        });
      }
    
      @override
      bool get wantKeepAlive => true;
    
      @override
      void initState() {
        super.initState();
        _rssListModel = RssListModel.fromChannel(widget.channel, _args());
    
        isConnected().then((internet) {
          if (!internet) {
            _showSnackBarWithDelay();
          } else {
            _addScrollControllerListener();
            setState(() {
              _rssFuture = initialize;
            });
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        super.build(context);
        return Container(
          padding: const EdgeInsets.symmetric(
            vertical: 8,
            horizontal: 16,
          ),
          color: Colors.white,
          child: FutureBuilder<List<RssItemModel?>?>(
            future: _rssFuture,
            builder: (context, snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.none:
                case ConnectionState.active:
                  break;
                case ConnectionState.waiting:
                  return getLoadingWidget();
                case ConnectionState.done:
                  {
                    if (!snapshot.hasData || snapshot.data!.isEmpty)
                      return _noDataView('No data to display');
                    if (snapshot.hasError)
                      return _noDataView("There was an error while fetching data");
    
                    return _refreshIndicator(snapshot);
                  }
              }
              return _noDataView('Unable to fetch data from server');
            },
          ),
        );
      }
    
      /// Returns a `RefreshIndicator` wrapping our `ListView`
      Widget _refreshIndicator(AsyncSnapshot snapshot) => RefreshIndicator(
            backgroundColor: const Color.fromARGB(255, 255, 255, 255),
            triggerMode: RefreshIndicatorTriggerMode.anywhere,
            color: MyColors.Red,
            onRefresh: _pullRefresh,
            child: _listView(snapshot),
          );
    
      /// Returns a `ListView` builder from an `AsyncSnapshot`
      Widget _listView(AsyncSnapshot snapshot) => ListView.builder(
            controller: _scrollController,
            clipBehavior: Clip.none,
            itemCount: snapshot.data!.length,
            physics: const BouncingScrollPhysics(),
            itemBuilder: (context, index) => RssCard(snapshot.data![index]),
          );
    
      /// Returns a `Widget` informing of "No Data Fetched"
      Widget _noDataView(String message) => Center(
            child: Text(
              message,
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w800,
              ),
            ),
          );
    }
    
  • Midas King
    Midas King about 2 years
    Could you possibly use a StreamBuilder for this? Or StreamBuilder doesn't hold on to a state?
  • Midas King
    Midas King about 2 years
    Thank you very much, I took your idea and implemented it... had to rework it so it's async and it works like a charm. Thank you very much.
  • Vandad Nahavandipoor
    Vandad Nahavandipoor about 2 years
    Fantastic. I'm glad you figured it out 🙏🏻
  • Randal Schwartz
    Randal Schwartz about 2 years
    your super.initState should be above the rest of the initState, not below'
  • Vandad Nahavandipoor
    Vandad Nahavandipoor about 2 years
    @RandalSchwartz thank you so much for your input. I've corrected that mistake now.
  • Randal Schwartz
    Randal Schwartz about 2 years
    Just remember it like "initState comes first, dispose comes last" and that will tell you where the super also comes within those.
  • Vandad Nahavandipoor
    Vandad Nahavandipoor about 2 years
    Yes that just makes sense when I think about it. Thank you