Flutter中的分页/无限滚动,具有缓存和实时失效功能

自从我开始搜索Flutter ListView库以来已经有很长时间了,该库将使我能够以一种聪明的方式使用分页。遗憾的是我还没有找到符合我标准的东西:

  1. :库不应简单地逐页增加列表,而必须具有固定大小的缓存,此缓存仅在当前需要的页面才加载并保留在内存中。
  2. :该库基本上应该接受一个函数,该函数返回表示页面的列表的未来。
  3. :Dart具有流,因此,库应该以某种方式使用失效处理能力,并在以响应方式更改数据时重新加载所需的一切。

基本上,我想要的功能类似于标准Android库中的PagedListAdapter + DataSource.Factory +

LiveData。我想出了小部件 :

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();

}

因为我需要知道项目的总数并处理无效,所以我想接受Stream<int>每次修改数据时返回实际列表大小的a 。

这是它的用法示例:

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"),

));

}

我将SQLite与Moor一起使用以检索数据(https://moor.simonbinder.eu/docs/)。

  • database.getCategories(10, 10 * pageIndex))是一种返回Future<List<Category>>代表页面的方法
  • database.countCategories().watchSingle() 是Stream在每次添加/更新/删除时都发出列表大小

你怎么看?我错过了一些错误吗?你会做不同的事情吗?也许以更简单/优雅/高效的方式?

谢谢

我使用LruMap根据pskink的建议制作了一个新版本。

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);

}

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);

}

你怎么看?

回答:

这是最新版本,感谢一些非常有用的建议

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));

}

}

这是一个新版本,可确保setState在卸载小部件时不会调用期货。

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));

}

}

有更好的主意吗?

以上是 Flutter中的分页/无限滚动,具有缓存和实时失效功能 的全部内容, 来源链接: utcz.com/qa/410675.html

回到顶部