Last active
August 9, 2022 20:50
-
-
Save slightfoot/edb50ee41ddacaa6f4e25f973bdf200a to your computer and use it in GitHub Desktop.
Paged ListView - Loads data async into the list. You'll notice if you scroll slowly you'll never see the loading spinner, this is because we specify a `cacheExtent` to the widget. `separatorBuilder` is completely optional. You can override the default loading spinner by specifying a `loadingBuilder`. Return null from the `itemLoader` to stop loa…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
import 'paged_listview.dart'; | |
void main() => runApp(MaterialApp(home: HomeScreen())); | |
class HomeScreen extends StatefulWidget { | |
@override | |
_HomeAppState createState() => _HomeAppState(); | |
} | |
class _HomeAppState extends State<HomeScreen> { | |
final GlobalKey<PagedListViewState> _pagedList = GlobalKey(); | |
int _page = 0; | |
int _max = 5; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('Load paging data'), | |
actions: <Widget>[ | |
IconButton( | |
icon: Icon(Icons.refresh), | |
onPressed: () { | |
_max += 3; | |
_pagedList.currentState.tryLoadingMoreData(); | |
}, | |
), | |
], | |
), | |
body: LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
assert(constraints.hasBoundedHeight); | |
return PagedListView<TestModel>( | |
key: _pagedList, | |
itemLoader: loadMore, | |
itemBuilder: (BuildContext context, int index, TestModel data) { | |
return ListTile( | |
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0), | |
title: Text(data.page), | |
subtitle: Text(data.item), | |
); | |
}, | |
separatorBuilder: (BuildContext context, int index) => Divider(height: 2.0), | |
// We wrap our PagedListView in a LayoutBuilder to get the height of the parent | |
// so we can supply a cacheExtent to the ListView so we load items before they | |
// are displayed on the screen. | |
cacheExtent: constraints.constrainHeight() * 2, | |
); | |
}, | |
), | |
); | |
} | |
Future<List<TestModel>> loadMore(List<TestModel> current) async { | |
print('Loading more: ${current.length}'); | |
if (_page < _max) { | |
await Future.delayed(Duration(seconds: 2)); | |
_page++; | |
return List.generate(10, (int index) => TestModel( | |
'Page $_page', | |
'Item $index', | |
)); | |
} | |
return null; | |
} | |
} | |
class TestModel { | |
final String page; | |
final String item; | |
const TestModel(this.page, this.item); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
typedef PagedItemBuilder<T> = Widget Function(BuildContext context, int index, T data); | |
typedef PagedItemLoader<T> = Future<List<T>> Function(List<T> current); | |
class PagedListView<T> extends StatefulWidget { | |
const PagedListView({ | |
Key key, | |
// ListView parameters | |
this.scrollDirection = Axis.vertical, | |
this.reverse = false, | |
this.controller, | |
this.primary, | |
this.physics, | |
this.shrinkWrap = false, | |
this.padding, | |
this.itemExtent, | |
this.addAutomaticKeepAlives = true, | |
this.addRepaintBoundaries = true, | |
this.cacheExtent, | |
// PagedListView specifics | |
@required this.itemBuilder, | |
@required this.itemLoader, | |
this.loadingBuilder = defaultLoadingBuilder, | |
this.separatorBuilder, | |
this.initialData, | |
}) : super(key: key); | |
final Axis scrollDirection; | |
final bool reverse; | |
final ScrollController controller; | |
final bool primary; | |
final ScrollPhysics physics; | |
final bool shrinkWrap; | |
final EdgeInsetsGeometry padding; | |
final double itemExtent; | |
final bool addAutomaticKeepAlives; | |
final bool addRepaintBoundaries; | |
final double cacheExtent; | |
final PagedItemBuilder<T> itemBuilder; | |
final PagedItemLoader<T> itemLoader; | |
final IndexedWidgetBuilder loadingBuilder; | |
final IndexedWidgetBuilder separatorBuilder; | |
final List<T> initialData; | |
static Widget defaultLoadingBuilder(BuildContext context, int index) { | |
return Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: Center( | |
child: CircularProgressIndicator(), | |
), | |
); | |
} | |
@override | |
PagedListViewState<T> createState() => PagedListViewState<T>(); | |
} | |
class PagedListViewState<T> extends State<PagedListView<T>> { | |
bool _loading = false; | |
bool _end = false; | |
List<T> _data; | |
bool get isLoading => _loading; | |
bool get hasFinished => _end; | |
@override | |
void initState() { | |
super.initState(); | |
_data = widget.initialData ?? <T>[]; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ListView.builder( | |
// ListView params passed-in | |
scrollDirection: widget.scrollDirection, | |
reverse: widget.reverse, | |
controller: widget.controller, | |
primary: widget.primary, | |
physics: widget.physics, | |
shrinkWrap: widget.shrinkWrap, | |
padding: widget.padding, | |
itemExtent: widget.itemExtent, | |
addAutomaticKeepAlives: widget.addAutomaticKeepAlives, | |
addRepaintBoundaries: widget.addRepaintBoundaries, | |
cacheExtent: widget.cacheExtent, | |
// PagedListView specifics | |
itemCount: _data.length * (widget.separatorBuilder != null ? 2 : 1) + 1, | |
itemBuilder: _itemBuilder, | |
); | |
} | |
Widget _itemBuilder(BuildContext context, int index) { | |
final int itemIndex = (widget.separatorBuilder != null) ? index ~/ 2 : index; | |
if (itemIndex < _data.length) { | |
return widget.separatorBuilder == null || index.isEven | |
? widget.itemBuilder(context, itemIndex, _data[itemIndex]) | |
: widget.separatorBuilder(context, itemIndex); | |
} else { | |
if (!_end && !_loading) { | |
tryLoadingMoreData(); | |
} | |
return _loading ? widget.loadingBuilder(context, itemIndex) : null; | |
} | |
} | |
void tryLoadingMoreData() async { | |
if (!_loading) { | |
_loading = true; | |
await WidgetsBinding.instance.endOfFrame; | |
setState(() => _end = false); | |
try { | |
List<T> data = await widget.itemLoader(List.unmodifiable<T>(_data)); | |
if (mounted && data != null) { | |
setState(() => _data.addAll(data)); | |
} | |
if (data == null) { | |
_end = true; | |
} | |
} finally { | |
setState(() => _loading = false); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment