refactor: extract state to list scaffold, keep business logic clean

This commit is contained in:
Rongjian Zhang 2019-02-09 13:29:44 +08:00
parent 84596d3fcf
commit 8d50862b52
8 changed files with 83 additions and 70 deletions

View File

@ -72,6 +72,7 @@ class _SettingsProviderState extends State<SettingsProvider> {
theme = _theme;
await prefs.setInt('theme', theme);
print('write theme: $theme');
setState(() {});
}
@ -161,12 +162,12 @@ class _SettingsProviderState extends State<SettingsProvider> {
}
int _theme = prefs.getInt('theme');
print('read theme: $_theme');
if (ThemeMap.all.contains(_theme)) {
theme = _theme;
} else if (Platform.isIOS) {
theme = ThemeMap.cupertino;
}
theme = ThemeMap.material;
setState(() {
ready = true;

View File

@ -5,39 +5,45 @@ import '../providers/settings.dart';
import '../widgets/error_reload.dart';
import '../widgets/loading.dart';
typedef RefreshCallback = Future<void> Function();
class ListPayload<T, K> {
K cursor;
List<T> items;
bool end;
ListPayload({this.items, this.cursor, this.end});
}
// This is a scaffold for infinite scroll screens
class ListScaffold extends StatefulWidget {
class ListScaffold<T, K> extends StatefulWidget {
final Widget title;
final IconData trailingIconData;
final Function trailingOnTap;
final Widget header;
final int itemCount;
final IndexedWidgetBuilder itemBuilder;
final RefreshCallback onRefresh;
final RefreshCallback onLoadMore;
// final IconData trailingIconData;
// final Function trailingOnTap;
final Widget Function(T payload) itemBuilder;
final Future<ListPayload<T, K>> Function() onRefresh;
final Future<ListPayload<T, K>> Function(K cursor) onLoadMore;
ListScaffold({
@required this.title,
this.trailingIconData,
this.trailingOnTap,
this.header,
@required this.itemCount,
// this.trailingIconData,
// this.trailingOnTap,
@required this.itemBuilder,
@required this.onRefresh,
this.onLoadMore,
@required this.onLoadMore,
});
@override
_ListScaffoldState createState() => _ListScaffoldState();
}
class _ListScaffoldState extends State<ListScaffold> {
class _ListScaffoldState<T, K> extends State<ListScaffold<T, K>> {
bool loading = false;
bool loadingMore = false;
String error = '';
List<T> items = [];
K cursor;
bool end;
ScrollController _controller = ScrollController();
@override
@ -61,7 +67,10 @@ class _ListScaffoldState extends State<ListScaffold> {
loading = true;
});
try {
await widget.onRefresh();
var _payload = await widget.onRefresh();
items = _payload.items;
cursor = _payload.cursor;
end = _payload.end;
} catch (err) {
// print(err);
error = err.toString();
@ -75,12 +84,15 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Future<void> _loadMore() async {
// print('list scaffold load more');
print('list scaffold load more');
setState(() {
loadingMore = true;
});
try {
await widget.onLoadMore();
var _payload = await widget.onLoadMore(cursor);
items.addAll(_payload.items);
cursor = _payload.cursor;
end = _payload.end;
} catch (err) {
print(err);
} finally {
@ -93,7 +105,7 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Widget _buildItem(BuildContext context, int index) {
if (index == 2 * widget.itemCount) {
if (index == 2 * items.length) {
return Loading(more: true);
}
@ -105,19 +117,21 @@ class _ListScaffoldState extends State<ListScaffold> {
);
}
return widget.itemBuilder(context, index ~/ 2);
return widget.itemBuilder(items[index ~/ 2]);
}
Widget _buildSliver(BuildContext context) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
return SliverToBoxAdapter(
child: ErrorReload(text: error, reload: _refresh),
);
} else if (loading) {
return SliverToBoxAdapter(child: Loading(more: false));
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
_buildItem,
childCount: 2 * widget.itemCount + 1,
childCount: 2 * items.length + 1,
),
);
}
@ -131,7 +145,7 @@ class _ListScaffoldState extends State<ListScaffold> {
} else {
return ListView.builder(
controller: _controller,
itemCount: 2 * widget.itemCount + 1,
itemCount: 2 * items.length + 1,
itemBuilder: _buildItem,
);
}
@ -144,9 +158,9 @@ class _ListScaffoldState extends State<ListScaffold> {
List<Widget> slivers = [
CupertinoSliverRefreshControl(onRefresh: widget.onRefresh)
];
if (widget.header != null) {
slivers.add(SliverToBoxAdapter(child: widget.header));
}
// if (widget.header != null) {
// slivers.add(SliverToBoxAdapter(child: widget.header));
// }
slivers.add(_buildSliver(context));
return CupertinoPageScaffold(
@ -173,14 +187,14 @@ class _ListScaffoldState extends State<ListScaffold> {
return Scaffold(
appBar: AppBar(
title: widget.title,
actions: widget.trailingIconData == null
? []
: <Widget>[
IconButton(
icon: Icon(widget.trailingIconData),
onPressed: widget.trailingOnTap,
)
],
// actions: widget.trailingIconData == null
// ? []
// : <Widget>[
// IconButton(
// icon: Icon(widget.trailingIconData),
// onPressed: widget.trailingOnTap,
// )
// ],
),
body: RefreshIndicator(
onRefresh: widget.onRefresh,

View File

@ -169,7 +169,8 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
Widget _buildSliver() {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
return SliverToBoxAdapter(
child: ErrorReload(text: error, reload: _refresh));
} else if (loading) {
return SliverToBoxAdapter(child: Loading(more: false));
} else {

View File

@ -42,8 +42,8 @@ class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
Widget _buildBody() {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return Loading(more: true);
} else if (payload == null) {
return Loading(more: false);
} else {
return widget.bodyBuilder(payload);
}

View File

@ -31,7 +31,7 @@ class RefreshStatelessScaffold extends StatelessWidget {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: onRefresh);
} else if (loading) {
return Loading(more: true);
return Loading(more: false);
} else {
return bodyBuilder();
}

View File

@ -11,40 +11,32 @@ class NewsScreen extends StatefulWidget {
}
class NewsScreenState extends State<NewsScreen> {
int page = 1;
List<Event> _events = [];
Future<List<Event>> fetchEvents(int page) async {
var settings = SettingsProvider.of(context);
var login = settings.activeLogin;
List data = await settings
.getWithCredentials('/users/$login/received_events?page=$page');
// print(data);
return data.map<Event>((item) => Event.fromJSON(item)).toList();
}
@override
Widget build(context) {
// FIXME: can't add generic type here. Don't know why
// type '(Event) => EventItem' is not a subtype of type '(dynamic) => Widget'
return ListScaffold(
title: Text('News'),
itemCount: _events.length,
itemBuilder: (context, index) => EventItem(_events[index]),
itemBuilder: (payload) => EventItem(payload),
onRefresh: () async {
page = 1;
var page = 1;
var items = await fetchEvents(page);
if (mounted) {
setState(() {
_events = items;
});
}
return ListPayload(
cursor: page + 1, end: items.length < 30, items: items);
},
onLoadMore: () async {
page = page + 1;
onLoadMore: (page) async {
var items = await fetchEvents(page);
if (mounted) {
setState(() {
_events.addAll(items);
});
}
return ListPayload(
cursor: page + 1, end: items.length < 30, items: items);
},
);
}

View File

@ -3,12 +3,13 @@ import 'package:flutter/cupertino.dart';
import '../screens/issue.dart';
import '../screens/pull_request.dart';
import '../screens/user.dart';
import 'link.dart';
// import 'link.dart';
import 'avatar.dart';
import '../utils/utils.dart';
class EventItem extends StatelessWidget {
final Event event;
EventItem(this.event);
TextSpan _buildRepo(BuildContext context) {

View File

@ -37,7 +37,7 @@ class RepoItem extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
Padding(padding: EdgeInsets.only(top: 6)),
Text(item['description']),
Text(item['description'] ?? 'No description provided yet'),
Padding(padding: EdgeInsets.only(top: 6)),
DefaultTextStyle(
style: TextStyle(color: Colors.black54, fontSize: 13),
@ -50,17 +50,21 @@ class RepoItem extends StatelessWidget {
size: 14, color: Colors.black54),
Text(item['forks']['totalCount'].toString()),
Padding(padding: EdgeInsets.only(left: 16)),
Container(
width: 10,
height: 10,
decoration: new BoxDecoration(
color:
convertColor(item['primaryLanguage']['color']),
shape: BoxShape.circle,
),
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(item['primaryLanguage']['name']),
item['primaryLanguage'] == null
? Container()
: Row(children: <Widget>[
Container(
width: 10,
height: 10,
decoration: new BoxDecoration(
color: convertColor(
item['primaryLanguage']['color']),
shape: BoxShape.circle,
),
),
Padding(padding: EdgeInsets.only(left: 4)),
Text(item['primaryLanguage']['name']),
]),
],
),
)