Last active
February 17, 2021 02:19
-
-
Save pskink/c9bbe21c68efb14eb1c01ba0a4d93414 to your computer and use it in GitHub Desktop.
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 '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