Skip to content
This repository has been archived by the owner on Aug 1, 2021. It is now read-only.

Commit

Permalink
feat: add loadMore mixin
Browse files Browse the repository at this point in the history
  • Loading branch information
boyan01 committed Jun 13, 2019
1 parent 5c37dc4 commit 267e872
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 161 deletions.
174 changes: 13 additions & 161 deletions lib/loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Widget Function(
BuildContext context, T result);
Expand All @@ -33,33 +40,16 @@ class Loader<T> extends StatefulWidget {
super(key: key);

static Widget buildSimpleLoadingWidget<T>(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: <Widget>[
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();
},
);
}

Expand Down Expand Up @@ -210,141 +200,3 @@ class LoaderResultWidget<T> extends StatelessWidget {
}
}
}

///a list view
///auto load more when reached the bottom
class AutoLoadMoreList<T> extends StatefulWidget {
///list total count
final totalCount;

///initial list item
final List<T> initialList;

///return the items loaded
///null indicator failed
final Future<List<T>> 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<T> createState() => _AutoLoadMoreListState<T>();
}

class _AutoLoadMoreListState<T> extends State<AutoLoadMoreList> {
///true when more item available
bool hasMore;

///true when load error occurred
bool error = false;

List<T> items = [];

CancelableOperation<List> _autoLoadOperation;

@override
AutoLoadMoreList<T> 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<List<T>>.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<ScrollUpdateNotification>(
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: <Widget>[
SizedBox(
child: CircularProgressIndicator(),
height: 16,
width: 16,
),
Padding(
padding: EdgeInsets.only(left: 8),
),
Text("正在加载更多...")
],
),
);
}
}
187 changes: 187 additions & 0 deletions lib/src/auto_loader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
part of '../loader.dart';

enum LoaderType { loading, error, empty }

mixin AutoLoadMoreMixin<T> on Model {
@protected
final List<T> _data = [];

bool _more = true;

///has more comments
bool get hasMore => _more;

int _offset = 0;

CancelableOperation _autoLoadOperation;

@protected
Future<Result<List<T>>> 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<Result<List<T>>>.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<T> extends ValueResult<List<T>> {
///已加载的数据条目
final int loaded;

final bool hasMore;

final dynamic payload;

LoadMoreResult(List<T> value, {int loaded, this.hasMore = true, this.payload})
: assert(value != null),
this.loaded = loaded ?? value.length,
super(value);

factory LoadMoreResult._from(ValueResult<List<T>> result) {
if (result is LoadMoreResult) {
return result;
}
return LoadMoreResult(result.value);
}

///utils method for result mapping
static Result<R> map<T, R>(Result<T> 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<T> = Future<Result<List<T>>> Function(int offset);
Loading

0 comments on commit 267e872

Please sign in to comment.