diff --git a/lib/main.dart b/lib/main.dart index 30d80e9..2ef6be7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,22 +52,18 @@ class _HomeState extends State { List _buildNavigationItems() { return [ - BottomNavigationBarItem( - icon: Icon(Icons.inbox), - title: Text('Inbox'), - ), BottomNavigationBarItem( icon: Icon(Icons.rss_feed), title: Text('News'), ), - BottomNavigationBarItem( - icon: Icon(Icons.search), - title: Text('Search'), - ), BottomNavigationBarItem( icon: _buildNotificationIcon(context), title: Text('Notification'), ), + BottomNavigationBarItem( + icon: Icon(Icons.search), + title: Text('Search'), + ), BottomNavigationBarItem( icon: Icon(Icons.person), title: Text('Me'), @@ -78,14 +74,12 @@ class _HomeState extends State { _buildScreen(int index) { switch (index) { case 0: - return InboxScreen(); - case 1: return NewsScreen(); + case 1: + return NotificationScreen(); case 2: return SearchScreen(); case 3: - return NotificationScreen(); - case 4: return ProfileScreen(); } } diff --git a/lib/providers/settings.dart b/lib/providers/settings.dart index e62646c..02d134e 100644 --- a/lib/providers/settings.dart +++ b/lib/providers/settings.dart @@ -30,7 +30,7 @@ class _SettingsProviderState extends State { if (Platform.isIOS) { layout = LayoutMap.cupertino; } - layout = LayoutMap.material; + // layout = LayoutMap.material; } @override diff --git a/lib/screens/inbox.dart b/lib/screens/inbox.dart index 9a24c2a..47fe93b 100644 --- a/lib/screens/inbox.dart +++ b/lib/screens/inbox.dart @@ -1,8 +1,136 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import '../widgets/list_scaffold.dart'; -import '../widgets/notification_item.dart'; import '../utils/utils.dart'; +import '../screens/issue.dart'; +import '../screens/pull_request.dart'; +import '../widgets/link.dart'; + +class NotificationPayload { + String type; + String owner; + String name; + int number; + String title; + String updateAt; + bool unread; + + NotificationPayload.fromJson(input) { + type = input['subject']['type']; + name = input['repository']['name']; + owner = input['repository']['owner']['login']; + + String url = input['subject']['url']; + String numberStr = url.split('/').lastWhere((_) => true); + number = int.parse(numberStr); + + title = input['subject']['title']; + updateAt = TimeAgo.formatFromString(input['updated_at']); + unread = input['unread']; + } +} + +class NotificationItem extends StatelessWidget { + const NotificationItem({ + Key key, + @required this.payload, + }) : super(key: key); + + final NotificationPayload payload; + + Widget _buildRoute() { + switch (payload.type) { + case 'Issue': + return IssueScreen(payload.number, payload.owner, payload.name); + case 'PullRequest': + return PullRequestScreen(payload.number, payload.owner, payload.name); + default: + // throw new Exception('Unhandled notification type: $type'); + return Text('test'); + } + } + + IconData _buildIconData() { + switch (payload.type) { + case 'Issue': + return Octicons.issue_opened; + // color: Color.fromRGBO(0x28, 0xa7, 0x45, 1), + case 'PullRequest': + return Octicons.git_pull_request; + // color: Color.fromRGBO(0x6f, 0x42, 0xc1, 1), + default: + return Octicons.person; + } + } + + @override + Widget build(BuildContext context) { + return Link( + onTap: () { + Navigator.of(context).push( + CupertinoPageRoute(builder: (context) => _buildRoute()), + ); + }, + child: Container( + padding: EdgeInsets.all(8), + // color: payload.unread ? Colors.white : Colors.black12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(right: 8, top: 20), + child: Icon(_buildIconData(), color: Colors.black45), + ), + Expanded( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payload.owner + + '/' + + payload.name + + ' #' + + payload.number.toString(), + style: TextStyle(fontSize: 13, color: Colors.black54), + ), + Padding(padding: EdgeInsets.only(top: 4)), + Text( + payload.title, + style: TextStyle(fontSize: 15), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + Padding(padding: EdgeInsets.only(top: 6)), + Text( + payload.updateAt, + style: TextStyle( + fontSize: 12, + // fontWeight: FontWeight.w300, + color: Colors.black54, + ), + ) + ], + ), + ), + ), + Column( + children: [ + Icon(Octicons.check, color: Colors.black45), + Icon(Octicons.unmute, color: Colors.black45) + ], + ), + ], + ), + ], + ), + ), + ); + } +} Future> fetchNotifications(int page) async { List items = @@ -10,6 +138,7 @@ Future> fetchNotifications(int page) async { return items.map((item) => NotificationPayload.fromJson(item)).toList(); } +/// [@deprecated] class InboxScreen extends StatefulWidget { @override _InboxScreenState createState() => _InboxScreenState(); @@ -27,17 +156,27 @@ class _InboxScreenState extends State { 2: 'All', }; + Future _refresh() async { + page = 1; + var items = await fetchNotifications(page); + setState(() { + _items = items; + }); + } + @override Widget build(BuildContext context) { return ListScaffold( title: Text('Inbox'), - onRefresh: () async { - page = 1; - var items = await fetchNotifications(page); - setState(() { - _items = items; - }); + trailingIconData: Octicons.check, + trailingOnTap: () async { + bool answer = await showConfim(context, 'Mark all as read?'); + if (answer == true) { + await putWithCredentials('/notifications'); + _refresh(); + } }, + onRefresh: _refresh, onLoadMore: () async { page = page + 1; var items = await fetchNotifications(page); diff --git a/lib/screens/issue.dart b/lib/screens/issue.dart index e8219c4..bdaf95d 100644 --- a/lib/screens/issue.dart +++ b/lib/screens/issue.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import '../utils/utils.dart'; import '../widgets/list_scaffold.dart'; import '../widgets/timeline_item.dart'; +import '../widgets/comment_item.dart'; Future queryIssue(int id, String owner, String name) async { var data = await query(''' @@ -41,7 +42,25 @@ class _IssueScreenState extends State { Map payload; Widget _buildHeader() { - return Text('issue'); + return Column(children: [ + Container( + // padding: EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payload['title'], + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + height: 1.2, + ), + ), + CommentItem(payload), + ], + ), + ) + ]); } get _fullName => widget.owner + '/' + widget.name; diff --git a/lib/screens/notifications.dart b/lib/screens/notifications.dart index 5d3db4a..00c04cc 100644 --- a/lib/screens/notifications.dart +++ b/lib/screens/notifications.dart @@ -1,17 +1,22 @@ -import 'package:flutter/material.dart' hide Notification; -import 'package:flutter/cupertino.dart' hide Notification; -import '../providers/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import '../widgets/refresh_scaffold.dart'; import '../providers/notification.dart'; import '../widgets/notification_item.dart'; -import '../widgets/loading.dart'; import '../widgets/list_group.dart'; +import '../widgets/link.dart'; import '../utils/utils.dart'; -class NotificationGroup { - String fullName; - List items = []; +Future> fetchNotifications([int page = 1]) async { + List items = await getWithCredentials('/notifications?page=$page&all=true'); + return items.map((item) => NotificationPayload.fromJson(item)).toList(); +} - NotificationGroup(this.fullName); +class NotificationGroup { + String repo; + List items = []; + + NotificationGroup(this.repo); } class NotificationScreen extends StatefulWidget { @@ -22,108 +27,98 @@ class NotificationScreen extends StatefulWidget { class NotificationScreenState extends State { int active = 0; bool loading = false; - List groups = []; - - @override - void initState() { - super.initState(); - Future.delayed(Duration(seconds: 0)).then((_) { - _onSwitchTab(context, 0); - }); - } - - Widget _buildGroupItem(BuildContext context, int index) { - if (loading) { - return Loading(more: false); - } - - var group = groups[index]; + Map groupMap = {}; + Widget _buildGroupItem(String key, NotificationGroup group) { + var repo = group.repo; return ListGroup( - title: Text( - group.fullName, - style: TextStyle(color: Colors.black, fontSize: 15), - ), - items: group.items, - itemBuilder: (item) => NotificationItem(payload: item), - ); + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + repo, + style: TextStyle(color: Colors.black, fontSize: 15), + ), + Link( + onTap: () async { + await putWithCredentials('/repos/$repo/notifications'); + await _refresh(); + }, + child: Icon( + Octicons.check, + color: Colors.black45, + size: 20, + ), + ), + ], + ), + items: group.items, + itemBuilder: (item, index) { + return NotificationItem( + payload: item, + markAsRead: () { + setState(() { + groupMap[key].items[index].unread = false; + }); + }, + ); + }); } - void _onSwitchTab(BuildContext context, int index) async { - setState(() { - active = index; - loading = true; - }); + Future _onSwitchTab(BuildContext context, int index) async { + // setState(() { + // active = index; + // loading = true; + // }); - var ns = await ghClient.activity - .listNotifications(all: index == 2, participating: index == 1) - .toList(); + var ns = await fetchNotifications(); - NotificationProvider.of(context).setCount(ns.length); + // NotificationProvider.of(context).setCount(ns.length); - Map groupMap = {}; + Map _groupMap = {}; ns.forEach((item) { - String repo = item.repository.fullName; - if (groupMap[repo] == null) { - groupMap[repo] = NotificationGroup(repo); + String repo = item.owner + '/' + item.name; + if (_groupMap[repo] == null) { + _groupMap[repo] = NotificationGroup(repo); } - groupMap[repo].items.add(item); + _groupMap[repo].items.add(item); }); setState(() { - groups = groupMap.values.toList(); - loading = false; + groupMap = _groupMap; + // loading = false; }); } + // TODO: filter + // CupertinoSegmentedControl( + // groupValue: active, + // onValueChanged: (index) => _onSwitchTab(context, index), + // children: { + // 0: Text('Unread'), + // 1: Text('Paticipating'), + // 2: Text('All') + // }, + // ) + + Future _refresh() async { + print('onrefresh'); + await _onSwitchTab(context, active); + } + @override Widget build(context) { - // NotificationBloc bloc = NotificationProvider.of(context); - TextStyle _textStyle = DefaultTextStyle.of(context).style; + return RefreshScaffold( + title: Text('Notifications'), + onRefresh: _refresh, + bodyBuilder: () { + var children = groupMap.entries + .map((entry) => _buildGroupItem(entry.key, entry.value)) + .toList(); - switch (SettingsProvider.of(context).layout) { - case LayoutMap.cupertino: - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: SizedBox.expand( - child: DefaultTextStyle( - style: _textStyle, - child: CupertinoSegmentedControl( - groupValue: active, - onValueChanged: (index) => _onSwitchTab(context, index), - children: { - 0: Text('Unread'), - 1: Text('Paticipating'), - 2: Text('All') - }, - ), - ), - ), - ), - child: SafeArea( - child: CustomScrollView(slivers: [ - CupertinoSliverRefreshControl(), - SliverSafeArea( - top: false, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - _buildGroupItem, - childCount: groups.length, - ), - ), - ), - ]), - ), - ); - default: - return Scaffold( - appBar: AppBar(title: Text('Notification')), - body: ListView.builder( - itemCount: groups.length, - itemBuilder: _buildGroupItem, - ), - ); - } + return Column(children: children); + }, + ); } } diff --git a/lib/screens/pull_request.dart b/lib/screens/pull_request.dart index 3fccaa5..0a2dccb 100644 --- a/lib/screens/pull_request.dart +++ b/lib/screens/pull_request.dart @@ -98,11 +98,13 @@ class _PullRequestScreenState extends State { child: Row( children: [ Icon(iconData, color: Colors.white, size: 15), - Text(text, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - )), + Text( + text, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), ], ), ); diff --git a/lib/screens/user.dart b/lib/screens/user.dart index 7231b86..bee2ebe 100644 --- a/lib/screens/user.dart +++ b/lib/screens/user.dart @@ -91,7 +91,7 @@ class _UserScreenState extends State { return ListGroup( title: Text(title), items: items, - itemBuilder: (item) => RepoItem(item), + itemBuilder: (item, _) => RepoItem(item), ); } diff --git a/lib/utils/github.dart b/lib/utils/github.dart index f709fa6..e028e05 100644 --- a/lib/utils/github.dart +++ b/lib/utils/github.dart @@ -36,6 +36,23 @@ Future postWithCredentials(String url, String body, return data; } +Future putWithCredentials(String url, + {String contentType, String body}) async { + var headers = {HttpHeaders.authorizationHeader: 'token $token'}; + if (contentType != null) { + headers[HttpHeaders.contentTypeHeader] = contentType; + } + final res = await http.put(prefix + url, headers: headers, body: body ?? {}); + final data = json.decode(res.body); + return data; +} + +Future patchWithCredentials(String url) async { + var headers = {HttpHeaders.authorizationHeader: 'token $token'}; + await http.patch(prefix + url, headers: headers); + return true; +} + Future query(String query) async { final res = await postWithCredentials('/graphql', json.encode({'query': query})); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 6a11649..d860b89 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -20,6 +20,61 @@ class Option { Option({this.value, this.widget}); } +Future showConfim(BuildContext context, String text) { + switch (SettingsProvider.of(context).layout) { + case LayoutMap.cupertino: + return showCupertinoDialog( + context: context, + builder: (context) { + return CupertinoAlertDialog( + title: Text(text), + actions: [ + CupertinoDialogAction( + child: const Text('cancel'), + isDefaultAction: true, + onPressed: () { + Navigator.pop(context, false); + }, + ), + CupertinoDialogAction( + child: const Text('OK'), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + }, + ); + default: + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Text( + text, + // style: dialogTextStyle + ), + actions: [ + FlatButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.pop(context, false); + }, + ), + FlatButton( + child: const Text('OK'), + onPressed: () { + Navigator.pop(context, true); + }, + ) + ], + ); + }, + ); + } +} + Future showOptions(BuildContext context, List> options) { var builder = (BuildContext context) { return CupertinoAlertDialog( diff --git a/lib/widgets/event_item.dart b/lib/widgets/event_item.dart index 2fda868..8d45316 100644 --- a/lib/widgets/event_item.dart +++ b/lib/widgets/event_item.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; import '../screens/screens.dart'; import '../utils/utils.dart'; import '../widgets/widgets.dart'; -import '../widgets/link.dart'; /// Events types: /// diff --git a/lib/widgets/link.dart b/lib/widgets/link.dart index 0998ac8..bfaf479 100644 --- a/lib/widgets/link.dart +++ b/lib/widgets/link.dart @@ -4,14 +4,15 @@ import '../providers/settings.dart'; class Link extends StatelessWidget { final Widget child; final GestureTapCallback onTap; + final Color bgColor; - Link({@required this.child, @required this.onTap}); + Link({@required this.child, @required this.onTap, this.bgColor}); @override Widget build(BuildContext context) { return Material( child: Ink( - color: Colors.white, + color: bgColor ?? Colors.white, child: InkWell( splashColor: SettingsProvider.of(context).layout == LayoutMap.cupertino diff --git a/lib/widgets/list_group.dart b/lib/widgets/list_group.dart index 4df36dd..a288511 100644 --- a/lib/widgets/list_group.dart +++ b/lib/widgets/list_group.dart @@ -3,16 +3,16 @@ import 'package:flutter/material.dart'; class ListGroup extends StatelessWidget { final Widget title; final List items; - final Widget Function(T item) itemBuilder; + final Widget Function(T item, int index) itemBuilder; ListGroup({this.title, this.items, this.itemBuilder}); - Widget _buildItem(T item) { + Widget _buildItem(MapEntry entry) { return Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Colors.black12)), ), - child: itemBuilder(item), + child: itemBuilder(entry.value, entry.key), ); } @@ -30,7 +30,7 @@ class ListGroup extends StatelessWidget { color: Color(0x10000000), child: title, ), - Column(children: items.map(_buildItem).toList()) + Column(children: items.asMap().entries.map(_buildItem).toList()) ], ), ), diff --git a/lib/widgets/list_scaffold.dart b/lib/widgets/list_scaffold.dart index 65028c0..9dbc0ef 100644 --- a/lib/widgets/list_scaffold.dart +++ b/lib/widgets/list_scaffold.dart @@ -2,12 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/widgets.dart'; import '../providers/settings.dart'; +import '../widgets/link.dart'; import 'loading.dart'; typedef RefreshCallback = Future Function(); class ListScaffold extends StatefulWidget { final Widget title; + final IconData trailingIconData; + final Function trailingOnTap; final Widget header; final int itemCount; final IndexedWidgetBuilder itemBuilder; @@ -16,6 +19,8 @@ class ListScaffold extends StatefulWidget { ListScaffold({ @required this.title, + this.trailingIconData, + this.trailingOnTap, this.header, @required this.itemCount, @required this.itemBuilder, @@ -47,7 +52,7 @@ class _ListScaffoldState extends State { } Future _refresh() async { - print('refresh'); + print('list scaffold refresh'); setState(() { loading = true; }); @@ -63,7 +68,7 @@ class _ListScaffoldState extends State { } Future _loadMore() async { - print('more'); + print('list scaffold load more'); setState(() { loadingMore = true; }); @@ -130,7 +135,18 @@ class _ListScaffoldState extends State { slivers.add(_buildSliver(context)); return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(middle: widget.title), + navigationBar: CupertinoNavigationBar( + middle: widget.title, + trailing: Link( + child: Icon( + widget.trailingIconData, + size: 24, + color: Colors.blueAccent, + ), + onTap: widget.trailingOnTap, + bgColor: Colors.transparent, + ), + ), child: SafeArea( child: CustomScrollView( controller: _controller, @@ -140,7 +156,17 @@ class _ListScaffoldState extends State { ); default: return Scaffold( - appBar: AppBar(title: widget.title), + appBar: AppBar( + title: widget.title, + actions: widget.trailingIconData == null + ? [] + : [ + IconButton( + icon: Icon(widget.trailingIconData), + onPressed: widget.trailingOnTap, + ) + ], + ), body: RefreshIndicator( onRefresh: widget.onRefresh, child: _buildBody(context), diff --git a/lib/widgets/notification_item.dart b/lib/widgets/notification_item.dart index 7864a73..12fa160 100644 --- a/lib/widgets/notification_item.dart +++ b/lib/widgets/notification_item.dart @@ -1,12 +1,12 @@ -import 'dart:core'; -import 'package:flutter/material.dart' hide Notification; -import 'package:flutter/cupertino.dart' hide Notification; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import '../utils/utils.dart'; import '../screens/issue.dart'; import '../screens/pull_request.dart'; import 'link.dart'; class NotificationPayload { + String id; String type; String owner; String name; @@ -16,6 +16,7 @@ class NotificationPayload { bool unread; NotificationPayload.fromJson(input) { + id = input['id']; type = input['subject']['type']; name = input['repository']['name']; owner = input['repository']['owner']['login']; @@ -30,13 +31,23 @@ class NotificationPayload { } } -class NotificationItem extends StatelessWidget { - const NotificationItem({ +class NotificationItem extends StatefulWidget { + final NotificationPayload payload; + final Function markAsRead; + + NotificationItem({ Key key, @required this.payload, + @required this.markAsRead, }) : super(key: key); - final NotificationPayload payload; + @override + _NotificationItemState createState() => _NotificationItemState(); +} + +class _NotificationItemState extends State { + NotificationPayload get payload => widget.payload; + bool loading = false; Widget _buildRoute() { switch (payload.type) { @@ -63,6 +74,14 @@ class NotificationItem extends StatelessWidget { } } + Widget _buildCheckIcon() { + return Icon( + payload.unread ? Octicons.check : Octicons.primitive_dot, + color: loading ? Colors.black12 : Colors.black45, + size: 20, + ); + } + @override Widget build(BuildContext context) { return Link( @@ -71,59 +90,43 @@ class NotificationItem extends StatelessWidget { CupertinoPageRoute(builder: (context) => _buildRoute()), ); }, - child: Container( - padding: EdgeInsets.all(8), - color: payload.unread ? Colors.white : Colors.black12, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.only(right: 8, top: 20), - child: Icon(_buildIconData(), color: Colors.black45), + child: Opacity( + opacity: payload.unread ? 1 : 0.5, + child: Container( + padding: EdgeInsets.all(8), + child: Row( + children: [ + Container( + padding: EdgeInsets.only(right: 8), + child: Icon(_buildIconData(), color: Colors.black45, size: 20), + ), + Expanded( + child: Text( + payload.title, + overflow: TextOverflow.ellipsis, ), - Expanded( - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - payload.owner + - '/' + - payload.name + - ' #' + - payload.number.toString(), - style: TextStyle(fontSize: 13, color: Colors.black54), - ), - Padding(padding: EdgeInsets.only(top: 4)), - Text( - payload.title, - style: TextStyle(fontSize: 15), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - Padding(padding: EdgeInsets.only(top: 6)), - Text( - payload.updateAt, - style: TextStyle( - fontSize: 12, - // fontWeight: FontWeight.w300, - color: Colors.black54, - ), - ) - ], - ), - ), - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon(Octicons.check, color: Colors.black45), - ), - ], - ), - ], + ), + Link( + child: _buildCheckIcon(), + onTap: () async { + if (payload.unread && !loading) { + setState(() { + loading = true; + }); + try { + await patchWithCredentials( + '/notifications/threads/' + payload.id); + widget.markAsRead(); + } finally { + setState(() { + loading = false; + }); + } + } + }, + ), + ], + ), ), ), ); diff --git a/lib/widgets/refresh_scaffold.dart b/lib/widgets/refresh_scaffold.dart index 5808517..ff19e52 100644 --- a/lib/widgets/refresh_scaffold.dart +++ b/lib/widgets/refresh_scaffold.dart @@ -36,15 +36,15 @@ class _RefreshScaffoldState extends State { setState(() { loading = true; }); - try { - await widget.onRefresh(); - } catch (err) { - print(err); - } finally { - setState(() { - loading = false; - }); - } + // try { + await widget.onRefresh(); + // } catch (err) { + // print(err); + // } finally { + setState(() { + loading = false; + }); + // } } Widget _buildBody(BuildContext context) { @@ -64,7 +64,7 @@ class _RefreshScaffoldState extends State { child: SafeArea( child: CustomScrollView( slivers: [ - CupertinoSliverRefreshControl(onRefresh: widget.onRefresh), + CupertinoSliverRefreshControl(onRefresh: _refresh), SliverToBoxAdapter(child: _buildBody(context)) ], ), @@ -74,8 +74,8 @@ class _RefreshScaffoldState extends State { return Scaffold( appBar: AppBar(title: widget.title), body: RefreshIndicator( - onRefresh: widget.onRefresh, - child: _buildBody(context), + onRefresh: _refresh, + child: SingleChildScrollView(child: _buildBody(context)), ), ); }