Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active August 9, 2022 20:50
Show Gist options
  • Save slightfoot/edb50ee41ddacaa6f4e25f973bdf200a to your computer and use it in GitHub Desktop.
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…
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);
}
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