getting the pages multible times from infinite scroll pagination package - flutter

im building an app that gets data from api and the api has pages so i used the infinite scroll pagination package but when the data appears , the first page data appears and when i scroll down the first page appears two times and the second page appears ,
when i scroll again , the first and second page appears with the third page etc
this is the code im using
import 'dart:async';
import 'package:MyCima/models/films_data_model.dart';
import 'package:MyCima/services/services.dart';
import 'package:flutter/material.dart';
import 'films_card.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class ShowsListDesign extends StatefulWidget {
final String filterName;
const ShowsListDesign(this.filterName, {Key? key}) : super(key: key);
#override
_ShowsListDesignState createState() => _ShowsListDesignState();
}
class _ShowsListDesignState extends State<ShowsListDesign> {
final ServicesClass _servicesClass = ServicesClass();
FilmsDataModel modelClass = FilmsDataModel();
final PagingController _pagingController = PagingController(firstPageKey: 1);
#override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
Future<void> _fetchPage(int pageKey) async {
try {
final List newItems =
await _servicesClass.getFilms('posts/$pageKey/${widget.filterName}');
final isLastPage = newItems.length < 20;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
_pagingController.appendPage(newItems, nextPageKey);
}
} catch (error) {
_pagingController.error = error;
}
}
#override
Widget build(BuildContext context) => PagedGridView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
modelClass = FilmsDataModel.fromJson(item);
return FilmsCard(
key: UniqueKey(),
image: modelClass.thumbUrl,
title: modelClass.title,
year: modelClass.year,
id: modelClass.id,
);
}),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
crossAxisSpacing: 24,
mainAxisSpacing: 24,
childAspectRatio: (2 / 3),
),
);
#override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

I think your mistake is from
"final isLastPage = newItems.length < 20;"
you comparison is from the new data fetched and not the total amount of posts fetched since the first time.
and be sure that your request give you the good response
this is my fetchPage
Future<void> fetchPage(int pageKey) async {
try {
final newPage = await _presenter.getWithPagination(pageKey, 10);
final previouslyFetchedWordCount =
_pagingController.itemList?.length ?? 0;
final isLastPage = newPage.isLastPage(previouslyFetchedWordCount);
final newItems = newPage.itemList;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
_pagingController.appendPage(newItems, nextPageKey);
}
} catch (error) {
_pagingController.error = error;
}

Related

Where to prevent re-animation of ListView.builder items when scrolling back to previously animated items?

I have a listview in which the text of items are animated when they first appear - and when they reappear after enough scrolling. When the list grows to certain size and the user scrolls back far enough items are animated again - presumably they've been removed from the widget tree and are now being re-inserted and thus get re-initiated etc. I want to prevent this from happening so that they only animate the first time they appear.
I think this means I need to have state stored somewhere per item that keeps track and tells the individual items whether they should animate on them being built or not. I am not sure where to put and how to connect that though, partly because it seems to overlap between presentation and business logic layers. I think perhaps it should be a variable in the list items contained in the list object that the listview builder is constructing from - or should it somehow be in the actual widgets in the listview?
class _StockListViewBuilderState extends State<StockListViewBuilder> with asc_alertBar {
final ScrollController _scrollController = ScrollController();
late double _scrollPosition;
late double _maxScrollExtent;
late bool isThisTheEnd = false;
_scrollListener() async {
setState(() {
_scrollPosition = _scrollController.position.pixels;
_maxScrollExtent = _scrollController.position.maxScrollExtent;
});
if (!isThisTheEnd && _scrollPosition / _maxScrollExtent > 0.90) {
isThisTheEnd = true;
if (widget.stockListicle.getIsRemoteEmpty()) {
alertBar('No more items available', /* null,*/ context: context);
} else {
await widget.stockListicle.fetch(numberToFetch: 5);
}
}
if (isThisTheEnd && _scrollPosition / _maxScrollExtent <= 0.90) {
isThisTheEnd = false;
}
}
#override
void initState() {
super.initState();
late String? userFullName = GetIt.I.get<Authenticate>().user?.fullName;
developer.log('Authenticated user $userFullName', name: '_StockListViewBuilderState');
developer.log("init ", name: "_StockListViewBuilderState ");
int listCount;
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) async {
//developer.log("stckLtcl init pf con ");
listCount = widget.stockListicle.items.length;
if (listCount < 10 && !widget.stockListicle.getIsRemoteEmpty()) {
try {
await widget.stockListicle.fetch(numberToFetch: 10);
} catch (e) {
super.setState(() {
//developer.log("Can't load stock:$e");
alertBar(
"Couldn't load from the internet.",
context: context,
backgroundColor: Colors.purple,
);
});
}
}
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
final ConnectionNotifier connectionNotifier = context.read<ConnectionNotifier>();
if (connectionNotifier.isConnected() != true) {
await connectionNotifier.check();
if (connectionNotifier.isConnected() != true) {
alertBar("Please check the internet connection.", context: context);
}
}
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
controller: _scrollController,
shrinkWrap: true,
key: widget.theKey,
itemCount: widget.stockListicle.items.length + 1,
itemBuilder: (context, index) {
if (index <= widget.stockListicle.items.length - 1) {
return InkWell(
onTap: (() => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(widget.stockListicle.items[index] as Stock),
)),
child: StockListItem(
stock: widget.stockListicle.items[index] as Stock,
));
} else {
return LoadingItemNotifier(
isLoading: widget.stockListicle.getIsBusyLoading(),
);
}
},
);
}
}
//...
Currently StockListItem extends StatelessWidget and returns a 'ListTile' which as its title parameter has ...title: AnimatedText(textContent: stock.title),...
I was trying to keep track of first-time-animation inside AnimatedText widget until I realized from an OOP & Flutter perspective, it's probably wrong place...
class AnimatedText extends StatefulWidget {
final bool doShowMe;
final String textContent;
final Duration hideDuration;
final double durationFactor;
const AnimatedText({
Key? key,
this.doShowMe = true,
this.textContent = '',
this.hideDuration = const Duration(milliseconds: 500),
this.durationFactor = 1,
}) : super(key: key);
#override
State<AnimatedText> createState() => _AnimatedTextState();
}
class _AnimatedTextState extends State<AnimatedText> with SingleTickerProviderStateMixin {
late AnimationController _appearanceController;
late String displayText;
late String previousText;
late double durationFactor;
late Duration buildDuration = Duration(
milliseconds: (widget.textContent.length / 15 * widget.durationFactor * 1000).round());
#override
void initState() {
super.initState();
developer.log('init ${widget.textContent}', name: '_AnimatedTextState');
displayText = '';
previousText = widget.textContent;
_appearanceController = AnimationController(
vsync: this,
duration: buildDuration,
)..addListener(
() => updateText(),
);
if (widget.doShowMe) {
_doShowMe();
}
}
void updateText() {
String payload = widget.textContent;
int numCharsToShow = (_appearanceController.value * widget.textContent.length).ceil();
if (widget.doShowMe) {
// make it grow
displayText = payload.substring(0, numCharsToShow);
// developer.log('$numCharsToShow / ${widget.textContent.length} ${widget.textContent}');
} else {
// make it shrink
displayText = payload.substring(payload.length - numCharsToShow, payload.length);
}
}
#override
void didUpdateWidget(AnimatedText oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.doShowMe != oldWidget.doShowMe) || (widget.textContent != oldWidget.textContent)) {
if (widget.doShowMe) {
_doShowMe();
} else {
_doHideMe();
}
}
if (widget.doShowMe && widget.textContent != previousText) {
previousText = widget.textContent;
developer.log('reset');
_appearanceController
..reset()
..forward();
}
}
#override
void dispose() {
_appearanceController.dispose();
displayText = '';
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _appearanceController,
builder: (context, child) {
return Text(displayText);
});
}
void _doShowMe() {
_appearanceController
..duration = buildDuration
..forward();
}
void _doHideMe() {
_appearanceController
..duration = widget.hideDuration
..reverse();
}
}

when I am calling the provider in the inistate return data got null

My question is.......When the app Started(initial start) circular progress indicator appears & didn't got any data.....My app has 3 tabs(using tabbar), but when I swicth between my tabs & when I came to the Home tab now I got the data & ui render according to the data....
I want when the app start get data & needs render the data....
Ui inistate
class Home extends StatefulWidget {
const Home({ Key? key }) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _page = 1;
void initState() {
var postProvider = Provider.of<PostProvider>(context, listen: false);
postProvider.initStreamsPost();
postProvider.fetchAllData(_page);
}
#override
Widget build(BuildContext context) {
return Consumer<PostProvider>(builder: (context, postsModel, child) {
print("builder length... ${postsModel.allData!.length}"); //when the first time app opens what I got print in terminal => builder length... 0
if (postsModel.allData != null && postsModel.allData!.length > 0) {
return ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: postsModel.allData?.length,
itemBuilder: (BuildContext context, int index) {
return PostCard(
);
});
}
return Center(child: CircularProgressIndicator());
}
}
}
changeNotifier class
class PostProvider extends ChangeNotifier {
int totalPages = 0;
int? get totalRecords => _postsFetcher!.totalPosts;
PostApi? _postApi;
PostsModel? _postsFetcher;
List<Post>? get allData =>
_postsFetcher?.posts != null ? _postsFetcher?.posts : [];
PostProvider() {
initStreamsPost();
}
initStreamsPost() async {
_postApi = await new PostApi();
_postsFetcher = await new PostsModel();
}
fetchAllData(pageNumber) async {
if ((totalPages == 0) || pageNumber <= totalPages) {
PostsModel itemModel = await _postApi!.getPost2(pageNumber);
print("have data");
if (_postsFetcher!.posts == null) {
totalPages = ((itemModel.totalPosts! - 1) / 4).ceil();
_postsFetcher = itemModel;
notifyListeners();
} else {
_postsFetcher?.posts?.addAll(itemModel.posts!);
_postsFetcher = _postsFetcher;
notifyListeners();
}
}
}
}
While using Provider in initState, prefer using it this way:
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var postProvider = Provider.of<PostProvider>(context, listen: false);
postProvider.initStreamsPost();
postProvider.fetchAllData(_page);
});
}

Changing a variable doesn't change the view in flutter

I have an array of image location strings.
I have an int called displayImage in my class, and when I initialize an instance of that class, I set the value of displayImage as 0. So I see the image corresponding to the first image in the array.
Then I call a function, which, after a timeout, changes the value of the int from 0 to 1, but I don't see the corresponding second image. The image doesn't update. How to get the image to change when the index displayImage of the array changes?
var images = ["amusement1.jpeg", "amusement2.jpg", "amusement3.png", "amusement4.jpeg",
"amusement5.jpeg", "amusement6.jpeg"];
class RandomWords extends StatefulWidget {
#override
_RandomWordsState createState() => _RandomWordsState(0);
}
class _RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = TextStyle(fontSize: 10.0);
int displayImage;
_RandomWordsState(firstImage) {
this.displayImage = firstImage;
}
Widget _buildRow(WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
)
);
}
final timeout = Duration(seconds: 3);
void handleTimeout() {
print(this.displayImage);
this.displayImage++;
print(this.displayImage);
}
switchImages () {
print("switching image");
const timeout = Duration(seconds: 3);
const ms = Duration(milliseconds: 1);
Timer startTimeout(milliseconds) {
var duration = milliseconds == null ? timeout : ms * milliseconds;
return Timer(duration, handleTimeout);
}
startTimeout(1000);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
});
}
#override
Widget build(BuildContext context) {
switchImages();
String imageUrl = "assets/amusement/" + images[displayImage];
final wordPair = WordPair.random();
return Image(image: AssetImage(imageUrl));
}
}
Try updating this:
void handleTimeout() {
print(this.displayImage);
this.displayImage++;
print(this.displayImage);
}
To this:
void handleTimeout() {
print(this.displayImage);
setState((){
this.displayImage++;
});
print(this.displayImage);
}
To make the Widget rebuild, you should trigger the change using the setState method.

Pagination / Infinite scrolling in Flutter with caching and realtime invalidation

It's been a long time since I started to search for a Flutter ListView library that will allow me to use pagination in a smart way. Sadly I haven't found anything that meets my criteria:
Smart pagination: the library should't simply increase a list page-by-page but must have a fixed size cache which load and keep in memory only the needed pages in the moment.
Async loading: the library should basically accept a function which returns a future of a list representing a page.
Real-time invalidation: Dart has streams, so somehow the library should use their power to handle invalidation and reload everything needed when data changes in a reactive way.
Basically I wanted something that acted like PagedListAdapter + DataSource.Factory + LiveData in the standard Android library.
I came up with the widget PagedListView:
import 'dart:math';
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class PagedListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
PagedListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_PagedListView<T> createState() => _PagedListView<T>();
}
class _PagedListView<T> extends State<PagedListView<T>> {
/// Represent the number of cached pages before and after the current page.
/// If edgeCachePageCount = 1 the total number of cached pages are 3 (one before + current + one after).
/// TODO calculate from pageSize
final int edgeCachePageCount = 2;
int get maxCachedPageCount => (edgeCachePageCount * 2) + 1;
int currentPage = 0;
List<T> items;
Object error;
int totalCount = -1;
/// Contains the page indexes which the fetching is started but not completed.
final progressPages = Set<int>();
/// Contains the page indexes already retrieved.
final cachedPages = Set<int>();
int limitStartIndex = -1;
int limitEndIndex = -1;
#override
void initState() {
super.initState();
items = List.filled(widget.pageSize * maxCachedPageCount, null);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
// Invalidate.
cachedPages.clear();
if (count > 0) {
_fetchPages(PageRequest.SAME);
}
setState(() {});
});
}
void _fetchPages(PageRequest pageRequest) {
Set<int> refreshIndexes = _getRefreshIndexes();
//Fimber.i("Refresh indexes are $refreshIndexes");
refreshIndexes.forEach((pageIndex) => _fetchPage(pageIndex, pageRequest));
}
Set<int> _getRefreshIndexes() {
return getRefreshIndexes(maxCachedPageCount, edgeCachePageCount, currentPage, widget.pageSize, totalCount);
}
_fetchPage(int index, PageRequest request) {
if (cachedPages.contains(index)) {
// We already have this page.
return;
}
if (!progressPages.contains(index)) {
//Fimber.i("Fetch page $index start");
progressPages.add(index);
widget.pageFuture(index).asStream().map((list) => PageResult<T>(index, request, list)).listen(_onData, onError: _onError);
}
}
void _onData(PageResult<T> data) {
if (data.items != null) {
if (!_getRefreshIndexes().contains(data.index)) {
progressPages.remove(data.index);
//Fimber.i("Skipping invalid page ${data.index}, currentPage = $currentPage, refreshIndexes = ${_getRefreshIndexes()}");
return;
}
//Fimber.i("Fetch page ${data.index} end");
if (cachedPages.length == maxCachedPageCount) {
// The cached page count is exceeded, remove the smallest / greatest page.
if (data.request == PageRequest.NEXT) {
int smallestPage = cachedPages.reduce(min);
cachedPages.remove(smallestPage);
//Fimber.i("Smallest page $smallestPage removed");
} else if (data.request == PageRequest.PREVIOUS) {
int greatestPage = cachedPages.reduce(max);
cachedPages.remove(greatestPage);
//Fimber.i("Greatest page $greatestPage removed");
} else {
int smallestPage = cachedPages.reduce(min);
int greatestPage = cachedPages.reduce(max);
int smallestPageDistance = currentPage - smallestPage;
int greatestPageDistance = greatestPage - currentPage;
if (smallestPageDistance >= greatestPageDistance) {
//Fimber.i("Smallest page $smallestPage removed, smallestPageDistance = $smallestPageDistance, greatestPageDistance = $greatestPageDistance");
cachedPages.remove(smallestPage);
} else {
//Fimber.i("Greatest page $greatestPage removed, smallestPageDistance = $smallestPageDistance, greatestPageDistance = $greatestPageDistance");
cachedPages.remove(greatestPage);
}
}
}
Set<int> tempCachedPages = cachedPages.toSet()..add(data.index);
// Put the result in the correct position.
int startIndex = widget.pageSize * (data.index % maxCachedPageCount);
items.setAll(startIndex, data.items);
//Fimber.i("Fetch page ${data.index} end, startIndex = $startIndex");
limitStartIndex = cachedPages.isEmpty ? 0 : tempCachedPages.reduce(min) * widget.pageSize;
//Fimber.i("limitStartIndex set to $limitStartIndex");
limitEndIndex = cachedPages.isEmpty ? -1 : (widget.pageSize * tempCachedPages.reduce(max)) + data.items.length - 1;
//Fimber.i("limitEndIndex set to $limitEndIndex");
cachedPages.add(data.index);
progressPages.remove(data.index);
//Fimber.i("Fetch page ${data.index} end, startIndex = $startIndex, cached pages ${cachedPages.toList()..sort()}, currentPage = $currentPage");
setState(() {});
}
}
void _onError(error) {
this.error = error;
setState(() {});
}
_fetchNewPage(int index) {
int newPage = index ~/ widget.pageSize;
PageRequest pageRequest = newPage > currentPage ? PageRequest.NEXT : (newPage < currentPage ? PageRequest.PREVIOUS : PageRequest.SAME);
/*pageRequest == PageRequest.NEXT
? Fimber.i("Fetch next page $newPage")
: (pageRequest == PageRequest.PREVIOUS ? Fimber.i("Fetch previous page $newPage") : null);*/
currentPage = newPage;
_fetchPages(pageRequest);
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
if (index < limitStartIndex || index > limitEndIndex) {
_fetchNewPage(index);
}
return _getListItem(context, index);
},
);
}
Widget _getListItem(BuildContext context, int realIndex) {
int pageIndex = realIndex ~/ widget.pageSize;
if (!cachedPages.contains(pageIndex)) {
return widget.placeholderBuilder(context);
}
int cachePageIndex = pageIndex % maxCachedPageCount;
int cacheIndex = (cachePageIndex * widget.pageSize) + (realIndex % widget.pageSize);
return widget.itemBuilder(context, realIndex, items[cacheIndex]);
}
}
enum PageRequest { NEXT, PREVIOUS, SAME }
class PageResult<T> {
/// Page index of this data.
final int index;
/// Represent the direction from the current page when the request was made.
final PageRequest request;
final List<T> items;
PageResult(this.index, this.request, this.items);
}
Set<int> getRefreshIndexes(int maxCachedPageCount, int edgeCachePageCount, int currentPage, int pageSize, int totalCount) {
List<int> temp = List.generate(min(maxCachedPageCount, (totalCount ~/ pageSize) + 1), (index) => index + (currentPage - edgeCachePageCount));
int minIndex = temp.reduce(min);
if (minIndex < 0) {
return temp.map((index) => index + minIndex.abs()).toSet();
}
int maxIndex = temp.reduce(max);
int maxPage = totalCount ~/ pageSize;
if (maxIndex > maxPage) {
return temp.map((index) => index - (maxIndex - maxPage)).toSet();
}
return temp.toSet();
}
Since I need to know the total amount of items and handle invalidation I figured to accept a Stream<int> which returns the real list size every time data is modified.
This is an example of how it's used:
class MyHomePage extends StatelessWidget {
final MyDatabase database = MyDatabase();
MyHomePage({Key key}) : super(key: key);
Random random = Random.secure();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: PagedListView(
pageSize: 10,
pageFuture: (pageIndex) =>
Future.delayed(Duration(milliseconds: (random.nextDouble() * 5000).toInt()), () => database.getCategories(10, 10 * pageIndex)),
countStream: database.countCategories().watchSingle(),
itemBuilder: _itemBuilder,
waitBuilder: _waitBuilder,
placeholderBuilder: _placeholderBuilder,
emptyResultBuilder: _emptyResultBuilder,
errorBuilder: _errorBuilder,
),
);
}
Widget _itemBuilder(BuildContext context, int index, Category item) => Container(
height: 60,
child: Center(
child: ListTile(
key: Key(item.id.toString()),
title: Text(item.description),
subtitle: Text("id = ${item.id}, index = $index")
),
),
);
Widget _waitBuilder(BuildContext context) => Center(child: CircularProgressIndicator());
Widget _placeholderBuilder(BuildContext context) => Container(
height: 60,
margin: EdgeInsets.all(8),
child: Center(
child: CircularProgressIndicator(),
));
Widget _emptyResultBuilder(BuildContext context) => Container(
margin: EdgeInsets.all(8),
child: Center(
child: Text("Empty"),
));
Widget _errorBuilder(BuildContext context) => Container(
color: Colors.red,
margin: EdgeInsets.all(8),
child: Center(
child: Text("Error"),
));
}
I'm using SQLite with Moor to retrieve data (https://moor.simonbinder.eu/docs/).
database.getCategories(10, 10 * pageIndex)) is a method returning the Future<List<Category>> representing a page
database.countCategories().watchSingle() is the Stream emitting the list size at every add/update/delete
What do you think?
Am I missing some bugs? Would you done things differently? Maybe in a more simple / elegant / performant way?
Thanks
UPDATE #1
I made a new version based on pskink suggestion using LruMap.
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
LazyListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_LazyListView<T> createState() => _LazyListView<T>();
}
class _LazyListView<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
Object error;
int totalCount = -1;
int currentPage = 0;
#override
void initState() {
super.initState();
map = LruMap<int, PageResult<T>>(maximumSize: 500 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
map.clear();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
currentPage = index ~/ widget.pageSize;
final pageResult = map[currentPage];
final value = pageResult == null ? null : pageResult.items[index % widget.pageSize];
final loading = (value == null);
if (loading) {
cache.get(currentPage, ifAbsent: _loadPage).then(reload);
return widget.placeholderBuilder(context);
}
return widget.itemBuilder(context, index, value);
},
);
}
Future<PageResult<T>> _loadPage(int index) {
Fimber.i("Start fetch page $index");
return widget.pageFuture(index).then((list) => PageResult(index, list));
}
reload(PageResult<T> value) {
// Avoid calling setState if it's not needed.
if ((value.index - currentPage).abs() > 2) {
// ATTENTION: 2 is an arbitrary value, the distance between the current page and the page in the future result should ensure correct refreshing.
// It should be greater if item widgets have a smaller height, can be smaller if item widgets have a greater height.
// TODO: make it configurable?
Fimber.i("Skipping refreshing for result of page ${value.index}, currentPage = $currentPage");
return;
}
setState(() {});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
UPDATE #2 based on pskink new comment
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
LazyListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_LazyListView<T> createState() => _LazyListView<T>();
}
class _LazyListView<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
Object error;
int totalCount = -1;
#override
void initState() {
super.initState();
map = LruMap<int, PageResult<T>>(maximumSize: 50 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
map.clear();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
int currentPage = index ~/ widget.pageSize;
final pageResult = map[currentPage];
final value = pageResult == null ? null : pageResult.items[index % widget.pageSize];
final loading = (value == null);
if (loading) {
cache.get(currentPage, ifAbsent: _loadPage).then(_reload);
return widget.placeholderBuilder(context);
}
return widget.itemBuilder(context, index, value);
},
);
}
Future<PageResult<T>> _loadPage(int index) {
Fimber.i("Start fetch page $index");
return widget.pageFuture(index).then((list) => PageResult(index, list));
}
_reload(PageResult<T> value) {
if (value.refreshed) {
// Avoid calling setState if already called.
Fimber.i("Skipping refreshing for result of page ${value.index}");
return;
}
setState(() {
value.refreshed = true;
});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
bool refreshed = false;
PageResult(this.index, this.items);
}
What dou you think?
This is the last version thanks to some very useful suggestions
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget ErrorBuilder(BuildContext context, dynamic error);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final IndexedWidgetBuilder placeholderBuilder;
final WidgetBuilder waitBuilder;
final WidgetBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
final double velocityThreshold;
LazyListView({
#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.placeholderBuilder,
this.waitBuilder,
this.emptyResultBuilder,
this.errorBuilder,
this.velocityThreshold = 128,
}) : assert(pageSize > 0),
assert(pageFuture != null),
assert(countStream != null),
assert(itemBuilder != null),
assert(placeholderBuilder != null),
assert(velocityThreshold >= 0);
#override
_LazyListViewState<T> createState() => _LazyListViewState<T>();
}
class _LazyListViewState<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
dynamic error;
int totalCount = -1;
bool _frameCallbackInProgress = false;
#override
void initState() {
super.initState();
_initCache();
widget.countStream.listen((int count) {
totalCount = count;
_initCache();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
//debugPrintBeginFrameBanner = true;
//debugPrintEndFrameBanner = true;
//print('build');
if (error != null && widget.errorBuilder != null) return widget.errorBuilder(context, error);
if (totalCount == -1 && widget.waitBuilder != null) return widget.waitBuilder(context);
if (totalCount == 0 && widget.emptyResultBuilder != null) return widget.emptyResultBuilder(context);
return ListView.builder(
physics: _LazyListViewPhysics(velocityThreshold: widget.velocityThreshold),
itemCount: max(totalCount, 0),
itemBuilder: (context, index) {
// print('builder $index');
var page = index ~/ widget.pageSize;
final pageResult = map[page];
final value = pageResult?.items?.elementAt(index % widget.pageSize);
if (value != null) {
return widget.itemBuilder(context, index, value);
}
// print('$index ${Scrollable.of(context).position.activity.velocity}');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
cache.get(page, ifAbsent: _loadPage).then(_reload).catchError(_error);
} else if (!_frameCallbackInProgress) {
_frameCallbackInProgress = true;
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context));
}
return widget.placeholderBuilder(context, index);
},
);
}
Future<PageResult<T>> _loadPage(int index) async {
print('load $index');
var list = await widget.pageFuture(index);
return PageResult(index, list);
}
void _initCache() {
map = LruMap<int, PageResult<T>>(maximumSize: 50 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
}
void _error(dynamic e, StackTrace stackTrace) {
if (widget.errorBuilder == null) {
throw e;
}
setState(() => error = e);
}
void _reload(PageResult<T> value) => _doReload(value.index);
void _deferredReload(BuildContext context) {
print('_deferredReload');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
_frameCallbackInProgress = false;
_doReload(-1);
} else {
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context), rescheduling: true);
}
}
void _doReload(int index) {
// print('reload $index');
setState(() {});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
class _LazyListViewPhysics extends AlwaysScrollableScrollPhysics {
final double velocityThreshold;
_LazyListViewPhysics({
#required this.velocityThreshold,
ScrollPhysics parent,
}) : super(parent: parent);
#override
recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
// print('velocityThreshold: $velocityThreshold');
return velocity.abs() > velocityThreshold;
}
#override
_LazyListViewPhysics applyTo(ScrollPhysics ancestor) {
// print('applyTo($ancestor)');
return _LazyListViewPhysics(velocityThreshold: velocityThreshold, parent: buildParent(ancestor));
}
}
UPDATE #1
This is a new version which ensures futures don't call setState if the widget is unmounted.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget ErrorBuilder(BuildContext context, dynamic error);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final IndexedWidgetBuilder placeholderBuilder;
final WidgetBuilder waitBuilder;
final WidgetBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
final double velocityThreshold;
LazyListView({
#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.placeholderBuilder,
this.waitBuilder,
this.emptyResultBuilder,
this.errorBuilder,
this.velocityThreshold = 128,
}) : assert(pageSize > 0),
assert(pageFuture != null),
assert(countStream != null),
assert(itemBuilder != null),
assert(placeholderBuilder != null),
assert(velocityThreshold >= 0);
#override
_LazyListViewState<T> createState() => _LazyListViewState<T>();
}
class _LazyListViewState<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
dynamic error;
int totalCount = -1;
bool _frameCallbackInProgress = false;
StreamSubscription<int> countStreamSubscription;
#override
void initState() {
super.initState();
_initCache();
countStreamSubscription = widget.countStream.listen((int count) {
totalCount = count;
print('totalCount = $totalCount');
_initCache();
setState(() {});
});
}
#override
void dispose() {
countStreamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
//debugPrintBeginFrameBanner = true;
//debugPrintEndFrameBanner = true;
//print('build');
if (error != null && widget.errorBuilder != null) {
return widget.errorBuilder(context, error);
}
if (totalCount == -1 && widget.waitBuilder != null) {
return widget.waitBuilder(context);
}
if (totalCount == 0 && widget.emptyResultBuilder != null) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
physics: _LazyListViewPhysics(velocityThreshold: widget.velocityThreshold),
itemCount: max(totalCount, 0),
itemBuilder: (context, index) {
// print('builder $index');
final page = index ~/ widget.pageSize;
final pageResult = map[page];
final value = pageResult?.items?.elementAt(index % widget.pageSize);
if (value != null) {
return widget.itemBuilder(context, index, value);
}
// print('$index ${Scrollable.of(context).position.activity.velocity}');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
cache.get(page, ifAbsent: _loadPage).then(_reload).catchError(_error);
} else if (!_frameCallbackInProgress) {
_frameCallbackInProgress = true;
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context));
}
return widget.placeholderBuilder(context, index);
},
);
}
Future<PageResult<T>> _loadPage(int index) async {
print('load $index');
var list = await widget.pageFuture(index);
return PageResult(index, list);
}
void _initCache() {
map = LruMap<int, PageResult<T>>(maximumSize: 512 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
}
void _error(dynamic e, StackTrace stackTrace) {
if (widget.errorBuilder == null) {
throw e;
}
if (this.mounted) {
setState(() => error = e);
}
}
void _reload(PageResult<T> value) => _doReload(value.index);
void _deferredReload(BuildContext context) {
print('_deferredReload');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
_frameCallbackInProgress = false;
_doReload(-1);
} else {
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context), rescheduling: true);
}
}
void _doReload(int index) {
print('reload $index');
if (this.mounted) {
setState(() {});
}
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
class _LazyListViewPhysics extends AlwaysScrollableScrollPhysics {
final double velocityThreshold;
_LazyListViewPhysics({
#required this.velocityThreshold,
ScrollPhysics parent,
}) : super(parent: parent);
#override
recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
// print('velocityThreshold: $velocityThreshold');
return velocity.abs() > velocityThreshold;
}
#override
_LazyListViewPhysics applyTo(ScrollPhysics ancestor) {
// print('applyTo($ancestor)');
return _LazyListViewPhysics(velocityThreshold: velocityThreshold, parent: buildParent(ancestor));
}
}
Anyone with a better idea?

How to cast to unknown generic runtime type (C# ChangeType equivalent)

I'm brand new to Flutter / Dart and I'm trying to build a reusable infinite scroller with placeholder loading. The class is as follows:
import 'dart:async';
import 'package:flutter/material.dart';
class PagedScroller<T> extends StatefulWidget {
final int limit;
final Future<List<T>> Function(int, int) getDataFunction;
final Widget Function(T) renderFunction;
final Widget Function() renderPlaceholderFunction;
PagedScroller(
{#required this.limit,
#required this.getDataFunction,
#required this.renderFunction,
#required this.renderPlaceholderFunction});
#override
_PagedScrollerState<T> createState() => _PagedScrollerState<T>();
}
class _PagedScrollerState<T> extends State<PagedScroller> {
int _offset = 0;
int _lastDataLength = 1; // Init to one so the first call can happen
List<dynamic> _items = [];
Future<List<dynamic>> _future;
bool _isInitializing = false;
bool _isInitialized = false;
bool _isLoading = false;
ScrollController _controller =
ScrollController(initialScrollOffset: 0.0, keepScrollOffset: true);
_PagedScrollerState();
void _init() {
_isInitializing = true;
_reset();
_controller.addListener(() {
bool loadMore = false;
if (_controller.position.maxScrollExtent == double.infinity) {
loadMore = _controller.offset == _controller.position.maxScrollExtent;
} else {
loadMore =
_controller.offset >= _controller.position.maxScrollExtent * 0.85;
}
// Only load more if it's not currently loading and we're not on the last page
// _lastDataLength should be 0 if there are no more pages
if (loadMore && !_isLoading && _lastDataLength > 0) {
_offset += widget.limit;
_load();
}
});
_load();
_isInitializing = false;
_isInitialized = true;
}
void _reset() {
// Clear things array and reset inital get-things link (without paging)
setState(() {
_future = _clearThings();
});
// Reload things
// Reset to initial GET link
_offset = 0;
}
void _load() {
setState(() {
_future = _loadPlaceholders();
_future = _loadData();
});
}
Future<List<dynamic>> _clearThings() async {
_items.clear();
return Future.value(_items);
}
Future<List<dynamic>> _loadPlaceholders() async {
// Add 20 empty placeholders to represent stuff that's currently loading
for (var i = 0; i < widget.limit; i++) {
_items.add(_Placeholder());
}
return Future.value(_items);
}
List<dynamic> _getInitialPlaceholders() {
var placeholders = List<dynamic>();
for (var i = 0; i < widget.limit; i++) {
placeholders.add(_Placeholder());
}
return placeholders;
}
Future<List<dynamic>> _loadData() async {
_setLoading(true);
var data = await widget.getDataFunction(widget.limit, _offset);
// When loading data is done, remove any placeholders
_items.removeWhere((item) => item is _Placeholder);
// If 0 items were returned, it's probably the last page
_lastDataLength = data.length;
for (var item in data) {
_items.add(item);
}
_setLoading(false);
return Future.value(_items);
}
void _setLoading(bool isLoading) {
if (!mounted) {
return;
}
setState(() {
_isLoading = isLoading;
});
}
Future<void> _refreshThings() async {
_reset();
_load();
return Future;
}
#override
Widget build(BuildContext context) {
if (!_isInitializing && !_isInitialized) {
_init();
}
return FutureBuilder(
future: _future,
initialData: _getInitialPlaceholders(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<dynamic> loadedItems = snapshot.data;
return RefreshIndicator(
onRefresh: _refreshThings,
child: ListView.builder(
itemCount: loadedItems.length,
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
var item = loadedItems[index];
if (item is _Placeholder) {
return widget.renderPlaceholderFunction();
} else if (item is T) {
// THIS IS THE LINE THAT FAILS
return widget.renderFunction(item);
}
return Text('Unknown item type');
},
),
);
}
return Container();
},
);
}
}
class _Placeholder {}
The line that fails above:
return widget.renderFunction(item);
Fails with the following:
type '(MyModel) => Widget' is not a subtype of type '(dynamic) => Widget'
I understand why this is happening. The compiler can't know that type T from my PagedScroller<T> is the same as type T from _PagedScrollerState<T>. As a result, Dart tries to be helpful and converts my callback function of type Widget Function(T) to Widget Function(dynamic).
I then figured "maybe I can fake it out" with the following since I know the T in PagedScroller<T> and _PagedScrollerState<T> are always the same:
var renderFunction = widget.renderFunction as Widget Function(T);
return renderFunction(item);
Interestingly, this gives me a warning:
Unnecessary cast.
Try removing the cast.
Yet it won't even run that line (crashes) with the following:
Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
https://github.com/flutter/flutter/issues/new?template=BUG.md
Changing everything to dynamic works a charm, but I really don't want to lose the readability of generics here if I don't have to.
Despite extensive searching, I can't find the equivalent of C#'s Convert.ChangeType where you can provide types at runtime so I can just do the cast I want and be done with it.
This seems like a really simple thing to achieve, but I'm stuck.
You can consume the scroller with this simple main.dart copy/pasted:
import 'package:flutter/material.dart';
import 'package:minimal_repros/paged_scroller.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
Future<List<MyModel>> getDataFunction(int limit, int offset) async {
var myModels = List<MyModel>();
// Simulate API call
await Future.delayed(Duration(milliseconds: 1000));
for (int i = 0; i < limit; i++) {
var myModel = MyModel();
myModel.count = i + offset;
myModel.firstName = 'Bob';
myModels.add(myModel);
}
return myModels;
}
Widget renderFunction(MyModel myModel) {
return Text(myModel.firstName);
}
Widget renderPlaceholderFunction() {
return Text('Loading');
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PagedScroller(
getDataFunction: getDataFunction,
renderFunction: renderFunction,
renderPlaceholderFunction: renderPlaceholderFunction,
limit: 20));
}
}
class MyModel {
int count;
String firstName;
}
In the declaration of your State class, you forgot to specify the generic parameter of the widget.
Instead of:
class _PagedScrollerState<T> extends State<PagedScroller> {
do:
class _PagedScrollerState<T> extends State<PagedScroller<T>> {