From 8d50862b52d743f245e1a6ae0b1a31c5f3dd8c44 Mon Sep 17 00:00:00 2001 From: Rongjian Zhang Date: Sat, 9 Feb 2019 13:29:44 +0800 Subject: [PATCH] refactor: extract state to list scaffold, keep business logic clean --- lib/providers/settings.dart | 3 +- lib/scaffolds/list.dart | 82 ++++++++++++++++------------ lib/scaffolds/long_list.dart | 3 +- lib/scaffolds/refresh.dart | 4 +- lib/scaffolds/refresh_stateless.dart | 2 +- lib/screens/news.dart | 28 ++++------ lib/widgets/event_item.dart | 3 +- lib/widgets/repo_item.dart | 28 ++++++---- 8 files changed, 83 insertions(+), 70 deletions(-) diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart index 0a5fc74..3af2f0c 100644 --- a/lib/providers/settings.dart +++ b/lib/providers/settings.dart @@ -72,6 +72,7 @@ class _SettingsProviderState extends State { theme = _theme; await prefs.setInt('theme', theme); + print('write theme: $theme'); setState(() {}); } @@ -161,12 +162,12 @@ class _SettingsProviderState extends State { } 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; diff --git a/lib/scaffolds/list.dart b/lib/scaffolds/list.dart index d1d0134..4a9ebc5 100644 --- a/lib/scaffolds/list.dart +++ b/lib/scaffolds/list.dart @@ -5,39 +5,45 @@ import '../providers/settings.dart'; import '../widgets/error_reload.dart'; import '../widgets/loading.dart'; -typedef RefreshCallback = Future Function(); +class ListPayload { + K cursor; + List items; + bool end; + + ListPayload({this.items, this.cursor, this.end}); +} // This is a scaffold for infinite scroll screens -class ListScaffold extends StatefulWidget { +class ListScaffold 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> Function() onRefresh; + final Future> 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 { +class _ListScaffoldState extends State> { bool loading = false; bool loadingMore = false; String error = ''; + List items = []; + K cursor; + bool end; + ScrollController _controller = ScrollController(); @override @@ -61,7 +67,10 @@ class _ListScaffoldState extends State { 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 { } Future _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 { } 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 { ); } - 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 { } 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 { List 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 { return Scaffold( appBar: AppBar( title: widget.title, - actions: widget.trailingIconData == null - ? [] - : [ - IconButton( - icon: Icon(widget.trailingIconData), - onPressed: widget.trailingOnTap, - ) - ], + // actions: widget.trailingIconData == null + // ? [] + // : [ + // IconButton( + // icon: Icon(widget.trailingIconData), + // onPressed: widget.trailingOnTap, + // ) + // ], ), body: RefreshIndicator( onRefresh: widget.onRefresh, diff --git a/lib/scaffolds/long_list.dart b/lib/scaffolds/long_list.dart index 65fc6db..a4cfa39 100644 --- a/lib/scaffolds/long_list.dart +++ b/lib/scaffolds/long_list.dart @@ -169,7 +169,8 @@ class _LongListScaffoldState extends State> { 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 { diff --git a/lib/scaffolds/refresh.dart b/lib/scaffolds/refresh.dart index 833b960..5f7a043 100644 --- a/lib/scaffolds/refresh.dart +++ b/lib/scaffolds/refresh.dart @@ -42,8 +42,8 @@ class _RefreshScaffoldState extends State> { 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); } diff --git a/lib/scaffolds/refresh_stateless.dart b/lib/scaffolds/refresh_stateless.dart index c2311b3..a61e726 100644 --- a/lib/scaffolds/refresh_stateless.dart +++ b/lib/scaffolds/refresh_stateless.dart @@ -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(); } diff --git a/lib/screens/news.dart b/lib/screens/news.dart index 0bc9257..0fbff72 100644 --- a/lib/screens/news.dart +++ b/lib/screens/news.dart @@ -11,40 +11,32 @@ class NewsScreen extends StatefulWidget { } class NewsScreenState extends State { - int page = 1; - List _events = []; - Future> 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((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); }, ); } diff --git a/lib/widgets/event_item.dart b/lib/widgets/event_item.dart index 9220d5d..310f01c 100644 --- a/lib/widgets/event_item.dart +++ b/lib/widgets/event_item.dart @@ -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) { diff --git a/lib/widgets/repo_item.dart b/lib/widgets/repo_item.dart index 585d271..a688af0 100644 --- a/lib/widgets/repo_item.dart +++ b/lib/widgets/repo_item.dart @@ -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: [ + 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']), + ]), ], ), )