Pagination / Infinite scrolling in Flutter with caching and realtime invalidation

后端 未结 1 403
盖世英雄少女心
盖世英雄少女心 2020-12-18 14:17

It\'s been a long time since I started to search for a Flutter ListView library that will allow me to use pagination in a smart way. Sadly I haven\'t found anything that mee

相关标签:
1条回答
  • 2020-12-18 15:01

    This is the last version thanks to some very useful suggestions

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

    UPDATE #1

    This is a new version which ensures futures don't call setState if the widget is unmounted.

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

    Anyone with a better idea?

    0 讨论(0)
提交回复
热议问题