自从我开始搜索Flutter ListView库以来已经有很长时间了,该库将使我能够以一种聪明的方式使用分页。遗憾的是我还没有找到符合我标准的东西:
基本上,我想要的功能类似于标准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>每次修改数据时返回实际列表大小的a 。
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
我使用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); }
更新#2
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在卸载小部件时不会调用期货。
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)); } }
有更好的主意吗?