Flutter appending fetched data to a listview inside a future builder

Issue

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

Solution

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

Answered By – Vandad Nahavandipoor

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.