gpt4 book ai didi

flutter - Flutter 中的分页/无限滚动,带缓存和实时失效

转载 作者:行者123 更新时间:2023-12-03 23:08:57 25 4
gpt4 key购买 nike

我已经很久没有开始搜索 Flutter ListView 库,它可以让我以智能的方式使用分页。可悲的是,我还没有找到任何符合我的标准的东西:

  • 智能分页 :库不应该简单地逐页增加列表,而必须具有固定大小的缓存,该缓存仅加载和保留当前需要的页面。
  • 异步加载 :库应该基本上接受一个函数,该函数返回代表页面的列表的 future 。
  • 实时失效 :Dart 有流,所以库应该以某种方式利用它们的能力来处理失效并在数据以 react 方式发生变化时重新加载所需的一切。

  • 基本上,我想要一些类似于标准 Android 库中的 PagedListAdapter + DataSource.Factory + LiveData 的东西。
    我想出了小部件 PagedListView :

    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>每次修改数据时都会返回实际列表大小。

    这是如何使用它的示例:

    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()是在每次添加/更新/删除时发出列表大小的流

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

    谢谢

    更新 #1

    我基于pskink做了一个新版本 suggestion使用 LruMap。

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

    更新 #2 基于 pskink 新 comment

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

    更新 #1

    这是一个新版本,可确保 future 不会调用 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 - Flutter 中的分页/无限滚动,带缓存和实时失效,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60074466/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com