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
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.