Skip to content

Instantly share code, notes, and snippets.

@pskink
Last active February 17, 2021 02:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pskink/c9bbe21c68efb14eb1c01ba0a4d93414 to your computer and use it in GitHub Desktop.
Save pskink/c9bbe21c68efb14eb1c01ba0a4d93414 to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
/*
class LazyList extends StatefulWidget {
@override
_LazyListState createState() => _LazyListState();
}
class _LazyListState extends State<LazyList> {
Map<int, String> map = LruMap<int, String>(maximumSize: 200);
MapCache<int, String> cache;
Random r = Random();
@override
void initState() {
super.initState();
cache = MapCache<int, String>(map: map);
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 600,
itemBuilder: (context, index) {
// print('builder $index');
var value = map[index];
var loading = value == null;
if (loading) {
cache.get(index, ifAbsent: loadIfAbsent).then(reload);
}
return ListTile(
trailing: loading? Icon(Icons.timer, size: 48, color: Colors.red) : null,
title: Text(loading? 'please wait...' : value),
subtitle: Text('item #$index'),
);
},
);
}
FutureOr<String> loadIfAbsent(int key) {
print('load data #$key');
return Future.delayed(Duration(milliseconds: 1000 + r.nextInt(3000)), () => 'fake data for item #$key');
}
reload(String value) => setState(() {});
}
*/
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));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment