I am new to Flutter so please bear with me. I have a paginated API that means on calling example.com?loaditems.php?page=0
will load first 10 items(podcasts list) and example.com?loaditems.php?page=1
will load items from 10 to 20 and so on.
I want StreamBuilder to fetch page 0 first then when the list reaches the bottom it should load page 1 and show it. To check if I have reached the last item in listview I am using ScrollController
of ListView.
Now I am using StreamBuilder, ListView, InheritedWidget in bloc pattern. I am not sure if I have implemented it correctly so I am gonna paste the entire code. My question is, is this is the correct BLOC pattern way to do it? If not then what is it? I also came across with this article: https://crossingthestreams.io/loading-paginated-data-with-list-views-in-flutter/ In the end it says "Update:" but I could not understand it much.
Here' the app's entry point:
void main() => runApp(new MaterialApp(
title: "XYZ",
theme: ThemeData(fontFamily: 'Lato'),
home: PodcastsProvider( //This is InheritedWidget
child: RecentPodcasts(), //This is the child of InheritedWidget
),
));
Here's the InheritedWidget PodcastsProvider:
class PodcastsProvider extends InheritedWidget{
final PodcastsBloc bloc; //THIS IS THE BLOC
PodcastsProvider({Key key, Widget child})
: bloc = PodcastsBloc(),
super(key: key, child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
static PodcastsBloc of(BuildContext context){
return (context.inheritFromWidgetOfExactType(PodcastsProvider) as
PodcastsProvider).bloc;
}
}
Here's the Bloc
class PodcastsBloc{
var _podcasts = PublishSubject<List<Podcast>>();
Observable<List<Podcast>> get podcasts =>_podcasts.stream;
getPodcasts(pageCount) async{
NetworkProvider provider = NetworkProvider();
var podcasts = await provider.getRecentPodcasts(pageCount);
_podcasts.sink.add(podcasts);
}
despose(){
_podcasts.close();
}
}
Here's the view part (child of InheritedWidget)
class RecentPodcasts extends StatefulWidget {
@override
_RecentPodcastsState createState() => _RecentPodcastsState();
}
class _RecentPodcastsState extends State<RecentPodcasts> {
ScrollController controller = ScrollController();
PodcastsBloc podcastsBloc;
bool isLoading = false;
List<Podcast> podcasts;
@override
void didChangeDependencies() {
super.didChangeDependencies();
podcastsBloc = PodcastsProvider.of(context);
podcastsBloc.getPodcasts(null);
controller.addListener((){
if(controller.position.pixels == controller.position.maxScrollExtent && !isLoading){
setState(() {
isLoading = true;
podcastsBloc.getPodcasts(podcasts[podcasts.length-1].id);
});
}
}); //listerner ends
}
Finally, build method of _RecentPodcastsState calls this:
Widget getRecentPodcastsList(PodcastsBloc podcastsBloc) {
return StreamBuilder(
stream: podcastsBloc.podcasts,
builder: (context, snapshot) {
//isLoading = false;
if (snapshot.hasData) {
podcasts.addAll(snapshot.data); //THIS IS A PROBLEM, THIS GIVES ME AN ERROR: flutter: Tried calling: addAll(Instance(length:20) of '_GrowableList')
return ListView.builder(
scrollDirection: Axis.vertical,
padding: EdgeInsets.zero,
controller: controller,
itemCount: podcasts.length,
itemBuilder: (context, index) {
return RecentPodcastListItem(podcasts[index]);
});
} else if (snapshot.hasError) {
//SHOW ERROR TEXT
return Text("Error!");
} else {
//LOADER GOES HERE
return Text(
"Loading...",
style: TextStyle(color: Colors.white),
);
}
},
);
}}
Dynamic ListView You can make a dynamically created ListView by using the ListView. builder() constructor. This will create the ListView items only when they need to be displayed on the screen. It works like an Android RecyclerView but is a lot easier to set up.
I am new to Flutter
Welcome!
First of all, I want to express my concern against paginated APIs, since podcasts can be added while the user scrolls the list, resulting in podcasts missing or being displayed twice.
Having that out of the way, I'd like to point out that your question is quite broadly phrased, so I'll describe my own, opinionated approach on how I would do state management in this particular use case. Sorry for not providing sources, but Flutter and BLoC pattern are two relatively new things, and applications like paginated loading still need to be explored.
I like your choice of BLoC pattern, although I'm not sure the entire list needs to rebuild every time some new podcasts loaded.
Also, the pedantically BLoC-y way of doing things entirely with Sink
s and Stream
s is sometimes overly complex.
Especially if there is no continual "stream of data" but rather just a single point of data transimission, Future
s do the job quite well.
That's why I'd generate a method in the BLoC that gets called every time a podcast needs to be displayed. It abstracts from the number of podcasts on a page or the concept of loading - it simply returns a Future<Podcast>
every time.
For example, consider a BLoC providing this method:
final _cache = Map<int, Podcast>();
final _downloaders = Map<int, Future<List<Podcast>>>();
/// Downloads the podcast, if necessary.
Future<Podcast> getPodcast(int index) async {
if (!_cache.containsKey(index)) {
final page = index / 10;
await _downloadPodcastsToCache(page);
}
if (!_cache.containsKey(index)) {
// TODO: The download failed, so you should probably provide a more
// meaningful error here.
throw Error();
}
return _cache[index];
}
/// Downloads a page of podcasts to the cache or just waits if the page is
/// already being downloaded.
Future<void> _downloadPodcastsToCache(int page) async {
if (!_downloaders.containsKey(page)) {
_downloaders[page] = NetworkProvider().getRecentPodcasts(page);
_downloaders[page].then((_) => _downloaders.remove(page));
}
final podcasts = await _downloaders[page];
for (int i = 0; i < podcasts.length; i++) {
_cache[10 * page + i] = podcasts[i];
}
}
This method provides a very simple API to your widget layer. So now, let's see how it look from the widget layer point of view. Assume, you have a PodcastView
widget that displays a Podcast
or a placeholder if podcast
is null
. Then you could easily write:
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (ctx, index) {
return FutureBuilder(
future: PodcastsProvider.of(ctx).getPodcast(index),
builder: (BuildContext ctx, AsyncSnapshot<Podcast> snapshot) {
if (snapshot.hasError) {
return Text('An error occurred while downloading this podcast.');
}
return PodcastView(podcast: snapshot.data);
}
);
}
);
}
Pretty simple, right?
Benefits of this solution compared to the one from your link:
TL;DR: What I like about that solution is that it's inherently flexible and modular because the widgets themselves are quite "dumb" - caching, loading etc. all happens in the background. Making use of that flexibility, with a little bit of work, you could easily achieve these features:
_cache.clear()
) and the podcasts will be re-fetched automatically.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With