From 267e872490dcb2e1769d0ec4287eeca653f9a05a Mon Sep 17 00:00:00 2001 From: boyan Date: Thu, 13 Jun 2019 16:08:22 +0800 Subject: [PATCH] feat: add loadMore mixin --- lib/loader.dart | 174 +++---------------------------- lib/src/auto_loader.dart | 187 ++++++++++++++++++++++++++++++++++ lib/src/auto_loader_list.dart | 84 +++++++++++++++ lib/src/widgets.dart | 46 +++++++++ 4 files changed, 330 insertions(+), 161 deletions(-) create mode 100644 lib/src/auto_loader.dart create mode 100644 lib/src/auto_loader_list.dart create mode 100644 lib/src/widgets.dart diff --git a/lib/loader.dart b/lib/loader.dart index b359d6c..03ad1d0 100644 --- a/lib/loader.dart +++ b/lib/loader.dart @@ -3,14 +3,21 @@ library loader; import 'dart:async'; import 'package:async/async.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:loader/src/widgets.dart'; import 'package:meta/meta.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'package:scoped_model/scoped_model.dart'; export 'package:async/async.dart' show Result; export 'package:async/async.dart' show ErrorResult; export 'package:async/async.dart' show ValueResult; +export 'src/auto_loader_list.dart'; + +part 'src/auto_loader.dart'; + ///build widget when Loader has completed loading... typedef LoaderWidgetBuilder = Widget Function( BuildContext context, T result); @@ -33,33 +40,16 @@ class Loader extends StatefulWidget { super(key: key); static Widget buildSimpleLoadingWidget(BuildContext context) { - return Container( - constraints: BoxConstraints(minHeight: 200), - child: Center( - child: CircularProgressIndicator(), - ), - ); + return SimpleLoading(height: 200); } static Widget buildSimpleFailedWidget( BuildContext context, ErrorResult result) { - return Container( - constraints: BoxConstraints(minHeight: 200), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(result.error.toString()), - SizedBox(height: 8), - RaisedButton( - child: Text(MaterialLocalizations.of(context) - .refreshIndicatorSemanticLabel), - onPressed: () { - Loader.of(context).refresh(); - }) - ], - ), - ), + return SimpleFailed( + message: result.error.toString(), + retry: () { + Loader.of(context).refresh(); + }, ); } @@ -210,141 +200,3 @@ class LoaderResultWidget extends StatelessWidget { } } } - -///a list view -///auto load more when reached the bottom -class AutoLoadMoreList extends StatefulWidget { - ///list total count - final totalCount; - - ///initial list item - final List initialList; - - ///return the items loaded - ///null indicator failed - final Future> Function(int loadedCount) loadMore; - - ///build list tile with item - final Widget Function(BuildContext context, T item) builder; - - const AutoLoadMoreList( - {Key key, - @required this.loadMore, - @required this.totalCount, - @required this.initialList, - @required this.builder}) - : super(key: key); - - @override - _AutoLoadMoreListState createState() => _AutoLoadMoreListState(); -} - -class _AutoLoadMoreListState extends State { - ///true when more item available - bool hasMore; - - ///true when load error occurred - bool error = false; - - List items = []; - - CancelableOperation _autoLoadOperation; - - @override - AutoLoadMoreList get widget => super.widget; - - @override - void initState() { - super.initState(); - items.clear(); - items.addAll(widget.initialList ?? []); - hasMore = items.length < widget.totalCount; - } - - void _load() { - if (hasMore && !error && _autoLoadOperation == null) { - _autoLoadOperation = - CancelableOperation>.fromFuture(widget.loadMore(items.length)) - ..value.then((result) { - if (result == null) { - error = true; - } else if (result.isEmpty) { - //assume empty represent end of list - hasMore = false; - } else { - items.addAll(result); - hasMore = items.length < widget.totalCount; - } - setState(() {}); - }).whenComplete(() { - _autoLoadOperation = null; - }).catchError((e) { - setState(() { - error = true; - }); - }); - } - } - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: (notification) { - if (notification.metrics.extentAfter < 500) { - _load(); - } - }, - child: ListView.builder( - itemCount: items.length + (hasMore ? 1 : 0), - itemBuilder: (context, index) { - if (index >= 0 && index < items.length) { - return widget.builder(context, items[index]); - } else if (index == items.length && hasMore) { - if (!error) { - return _ItemLoadMore(); - } else { - return Container( - height: 56, - child: Center( - child: RaisedButton( - onPressed: () { - error = false; - _load(); - }, - child: Text("加载失败!点击重试"), - textColor: Theme.of(context).primaryTextTheme.body1.color, - color: Theme.of(context).errorColor, - ), - ), - ); - } - } - throw Exception("illegal state"); - }), - ); - } -} - -///suffix of a list, indicator that list is loading more items -class _ItemLoadMore extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - child: CircularProgressIndicator(), - height: 16, - width: 16, - ), - Padding( - padding: EdgeInsets.only(left: 8), - ), - Text("正在加载更多...") - ], - ), - ); - } -} diff --git a/lib/src/auto_loader.dart b/lib/src/auto_loader.dart new file mode 100644 index 0000000..fe6212c --- /dev/null +++ b/lib/src/auto_loader.dart @@ -0,0 +1,187 @@ +part of '../loader.dart'; + +enum LoaderType { loading, error, empty } + +mixin AutoLoadMoreMixin on Model { + @protected + final List _data = []; + + bool _more = true; + + ///has more comments + bool get hasMore => _more; + + int _offset = 0; + + CancelableOperation _autoLoadOperation; + + @protected + Future>> loadData(int offset); + + int get offset => _offset; + + dynamic error; + + List get items { + final items = List.from(_data); + if (error != null) { + items.add(LoaderType.error); + return items; + } + if (_autoLoadOperation != null) { + items.add(LoaderType.loading); + return items; + } + + if (items.isEmpty) { + return const [LoaderType.empty]; + } + return items; + } + + int get size => items.length; + + /// + /// load more items + /// + /// [notification] use notification to check is need load more items + /// + void loadMore({ScrollEndNotification notification}) { + if (error != null) { + return; + } + + if (notification != null && + (!_more || + notification.metrics.extentAfter > 500 || + _autoLoadOperation != null)) { + return; + } + + final offset = this.offset; + _autoLoadOperation = + CancelableOperation>>.fromFuture(loadData(offset)) + ..value.then((r) { + if (r == null || r.isError) { + error = r == null ? "result is null" : r.asError.error.toString(); + } else { + final result = LoadMoreResult._from(r.asValue); + _more = result.hasMore; + _offset += result.loaded; + _data.addAll(result.value); + + onDataLoaded(offset, result); + } + }).whenComplete(() { + notifyListeners(); + _autoLoadOperation = null; + }); + notifyListeners(); + } + + @protected + void onDataLoaded(int offset, LoadMoreResult result) {} + + ///create builder for [ListView] + IndexedWidgetBuilder createBuilder(List data, + {IndexedWidgetBuilder builder}) { + return (context, index) { + final widget = buildItem(context, data, index) ?? + (builder == null ? null : builder(context, index)); + assert(widget != null, 'can not build ${data[index]}'); + return widget; + }; + } + + IndexedWidgetBuilder obtainBuilder() { + return (context, index) { + return buildItem(context, items, index); + }; + } + + ///build item for position [index] + /// + /// return null if you do not care this position + /// + @protected + Widget buildItem(BuildContext context, List list, int index) { + if (list[index] == LoaderType.loading) { + return buildLoadingItem(context, list.length == 1); + } else if (list[index] == LoaderType.error) { + return buildErrorItem(context, list.length == 1); + } else if (list[index] == LoaderType.empty) { + return buildEmptyWidget(context); + } + return null; + } + + @protected + Widget buildLoadingItem(BuildContext context, bool empty) { + return SimpleLoading(height: empty ? 200 : 50); + } + + @protected + Widget buildErrorItem(BuildContext context, bool isEmpty) { + final retry = () { + error = null; + loadMore(); + }; + if (isEmpty) { + return SimpleFailed( + retry: retry, + message: error.toString(), + ); + } else { + return Container( + height: 56, + child: Center( + child: RaisedButton( + onPressed: retry, + child: Text("加载失败!点击重试"), + textColor: Theme.of(context).primaryTextTheme.body1.color, + color: Theme.of(context).errorColor, + ), + ), + ); + } + } + + @protected + Widget buildEmptyWidget(BuildContext context) { + return Container( + constraints: BoxConstraints(minHeight: 200), + child: Center(child: Text('暂无数据...')), + ); + } +} + +class LoadMoreResult extends ValueResult> { + ///已加载的数据条目 + final int loaded; + + final bool hasMore; + + final dynamic payload; + + LoadMoreResult(List value, {int loaded, this.hasMore = true, this.payload}) + : assert(value != null), + this.loaded = loaded ?? value.length, + super(value); + + factory LoadMoreResult._from(ValueResult> result) { + if (result is LoadMoreResult) { + return result; + } + return LoadMoreResult(result.value); + } + + ///utils method for result mapping + static Result map(Result result, R Function(T source) map) { + if (result.isError) return result.asError; + return Result.value(map(result.asValue.value)); + } +} + +///delegate to load more item +///[offset] loaded data length +typedef LoadMoreDelegate = Future>> Function(int offset); diff --git a/lib/src/auto_loader_list.dart b/lib/src/auto_loader_list.dart new file mode 100644 index 0000000..5f70a1c --- /dev/null +++ b/lib/src/auto_loader_list.dart @@ -0,0 +1,84 @@ +import 'package:loader/loader.dart'; +import 'package:flutter/material.dart'; + +import 'dart:async'; +import 'package:async/async.dart'; +import 'package:scoped_model/scoped_model.dart'; + +///a list view +///auto load more when reached the bottom +class AutoLoadMoreList extends StatefulWidget { + ///return the items loaded + ///null indicator failed + final LoadMoreDelegate loadMore; + + ///build list tile with item + final Widget Function(BuildContext context, T item) builder; + + const AutoLoadMoreList( + {Key key, @required this.loadMore, @required this.builder}) + : super(key: key); + + @override + _AutoLoadMoreListState createState() => _AutoLoadMoreListState(); +} + +class _AutoLoadMoreList extends Model with AutoLoadMoreMixin { + final LoadMoreDelegate delegate; + + _AutoLoadMoreList({@required this.delegate}) { + loadMore(); + } + + @override + Future>> loadData(int offset) { + return delegate(offset); + } +} + +class _AutoLoadMoreListState extends State> { + _AutoLoadMoreList _autoLoader; + + @override + void initState() { + super.initState(); + _autoLoader = _AutoLoadMoreList(delegate: widget.loadMore); + _autoLoader.addListener(_onDataChanged); + } + + @override + void didUpdateWidget(AutoLoadMoreList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.loadMore != widget.loadMore) { + _autoLoader.removeListener(_onDataChanged); + _autoLoader = _AutoLoadMoreList(delegate: widget.loadMore); + _autoLoader.addListener(_onDataChanged); + } + } + + @override + void dispose() { + _autoLoader.removeListener(_onDataChanged); + super.dispose(); + } + + void _onDataChanged() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + _autoLoader.loadMore(notification: notification); + return false; + }, + child: ListView.builder( + itemCount: _autoLoader.size, + itemBuilder: _autoLoader.createBuilder(_autoLoader.items, + builder: (context, index) { + return widget.builder(context, _autoLoader.items[index]); + })), + ); + } +} diff --git a/lib/src/widgets.dart b/lib/src/widgets.dart new file mode 100644 index 0000000..a057b16 --- /dev/null +++ b/lib/src/widgets.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class SimpleLoading extends StatelessWidget { + final double height; + + const SimpleLoading({Key key, this.height = 200}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(minHeight: height), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } +} + +class SimpleFailed extends StatelessWidget { + final VoidCallback retry; + + final String message; + + const SimpleFailed({Key key, this.retry, this.message}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(minHeight: 200), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + message != null ? Text(message) : Container(), + SizedBox(height: 8), + RaisedButton( + child: Text(MaterialLocalizations.of(context) + .refreshIndicatorSemanticLabel), + onPressed: retry, + ) + ], + ), + ), + ); + } +}