Flutter appending fetched data to a listview inside a future builder
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),
);
},
);
},
),
);
}
}
Midas King
Updated on January 03, 2023Comments
-
Midas King over 1 year
I'd like to ask when using the
FutureBuilder
to display fetched data from a remote server in aListView
. I check if the bottom of theListView
was reached usingScrollController
. Everything is working well until I try to load new data and append them to the existingListView
I fetch the data add them to myArray
and the insetState((){})
I update the list for theFutureBuilder
this is obviously the wrong approach since then the wholeFutureBuilder
is rebuilt and so is theListView
. The changes however do appear all the new items are in the list as intended however it slows performance not significantly sinceListView
is not keeping tiles out of view active but it has a small impact on performance, but the main issue is that sinceListView
gets rebuilt, I'm thrown as a user to the start of this list that's because theListView
got rebuilt. Now what I would like to achieve is that theListView
doesn't get rebuilt every time I get new data. Here is the code of the wholeStateFulWidget
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 about 2 yearsCould you possibly use a StreamBuilder for this? Or StreamBuilder doesn't hold on to a state?
-
Midas King about 2 yearsThank 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 about 2 yearsFantastic. I'm glad you figured it out 🙏🏻
-
Randal Schwartz about 2 yearsyour super.initState should be above the rest of the initState, not below'
-
Vandad Nahavandipoor about 2 years@RandalSchwartz thank you so much for your input. I've corrected that mistake now.
-
Randal Schwartz about 2 yearsJust remember it like "initState comes first, dispose comes last" and that will tell you where the super also comes within those.
-
Vandad Nahavandipoor about 2 yearsYes that just makes sense when I think about it. Thank you