feat: add error screen to handle network and other errors

This commit is contained in:
Rongjian Zhang 2019-02-08 20:52:10 +08:00
parent ddb9497ab8
commit 0a26509cdd
10 changed files with 252 additions and 121 deletions

View File

@ -157,7 +157,7 @@ class _SettingsProviderState extends State<SettingsProvider> {
} else if (Platform.isIOS) {
theme = ThemeMap.cupertino;
}
theme = ThemeMap.material;
// theme = ThemeMap.material;
setState(() {
ready = true;
@ -173,6 +173,10 @@ class _SettingsProviderState extends State<SettingsProvider> {
});
}
// http timeout
var _timeoutDuration = Duration(seconds: 10);
// var _timeoutDuration = Duration(seconds: 1);
Future<dynamic> query(String query, [String _token]) async {
if (_token == null) {
_token = token;
@ -181,12 +185,15 @@ class _SettingsProviderState extends State<SettingsProvider> {
throw Exception('token is null');
}
final res = await http.post(prefix + '/graphql',
headers: {
HttpHeaders.authorizationHeader: 'token $_token',
HttpHeaders.contentTypeHeader: 'application/json'
},
body: json.encode({'query': query}));
final res = await http
.post(prefix + '/graphql',
headers: {
HttpHeaders.authorizationHeader: 'token $_token',
HttpHeaders.contentTypeHeader: 'application/json'
},
body: json.encode({'query': query}))
.timeout(_timeoutDuration);
final data = json.decode(res.body);
if (data['errors'] != null) {
@ -202,10 +209,9 @@ class _SettingsProviderState extends State<SettingsProvider> {
// https://developer.github.com/v3/repos/contents/#custom-media-types
headers[HttpHeaders.contentTypeHeader] = contentType;
}
final res = await http.get(
prefix + url,
headers: headers,
);
final res = await http
.get(prefix + url, headers: headers)
.timeout(_timeoutDuration);
print(res.body);
final data = json.decode(res.body);
return data;
@ -213,15 +219,18 @@ class _SettingsProviderState extends State<SettingsProvider> {
Future<dynamic> patchWithCredentials(String url) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
await http.patch(prefix + url, headers: headers);
await http.patch(prefix + url, headers: headers).timeout(_timeoutDuration);
return true;
}
Future<dynamic> putWithCredentials(String url,
{String contentType, String body}) async {
var headers = {HttpHeaders.authorizationHeader: 'token $token'};
final res =
await http.put(prefix + url, headers: headers, body: body ?? {});
final res = await http
.put(prefix + url, headers: headers, body: body ?? {})
.timeout(_timeoutDuration);
print(res.body);
// final data = json.decode(res.body);
// return data;

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
import '../widgets/link.dart';
import '../widgets/error_reload.dart';
import '../widgets/loading.dart';
typedef RefreshCallback = Future<void> Function();
@ -36,6 +36,8 @@ class ListScaffold extends StatefulWidget {
class _ListScaffoldState extends State<ListScaffold> {
bool loading = false;
bool loadingMore = false;
String error = '';
ScrollController _controller = ScrollController();
@override
@ -55,12 +57,13 @@ class _ListScaffoldState extends State<ListScaffold> {
Future<void> _refresh() async {
print('list scaffold refresh');
setState(() {
error = '';
loading = true;
});
try {
await widget.onRefresh();
} catch (err) {
print(err);
error = err.toString();
} finally {
if (mounted) {
setState(() {
@ -105,7 +108,9 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Widget _buildSliver(BuildContext context) {
if (loading) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return SliverToBoxAdapter(child: Loading(more: false));
} else {
return SliverList(
@ -118,7 +123,9 @@ class _ListScaffoldState extends State<ListScaffold> {
}
Widget _buildBody(BuildContext context) {
if (loading) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return Loading(more: false);
} else {
return ListView.builder(

View File

@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
import '../widgets/loading.dart';
import '../widgets/link.dart';
import '../widgets/error_reload.dart';
class LongListPayload<T, K> {
T header;
@ -51,6 +52,8 @@ class LongListScaffold<T, K> extends StatefulWidget {
class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
bool loading;
bool loadingMore = false;
String error = '';
LongListPayload<T, K> payload;
@override
@ -62,10 +65,17 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
Future<void> _refresh() async {
print('long list scaffold refresh');
setState(() {
error = '';
loading = true;
});
try {
payload = await widget.onRefresh();
} catch (err) {
if (mounted) {
setState(() {
error = err.toString();
});
}
} finally {
if (mounted) {
setState(() {
@ -161,7 +171,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
}
Widget _buildSliver() {
if (loading) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return SliverToBoxAdapter(child: Loading(more: false));
} else {
return SliverList(
@ -172,7 +184,9 @@ class _LongListScaffoldState<T, K> extends State<LongListScaffold<T, K>> {
}
Widget _buildBody() {
if (loading) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return Loading(more: false);
} else {
return ListView.builder(itemCount: _itemCount, itemBuilder: _buildItem);

View File

@ -3,16 +3,14 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
import '../widgets/loading.dart';
typedef RefreshCallback = Future<void> Function();
typedef WidgetBuilder = Widget Function();
import '../widgets/error_reload.dart';
import '../utils/utils.dart';
// This is a scaffold for pull to refresh
class RefreshScaffold extends StatelessWidget {
class RefreshScaffold<T> extends StatefulWidget {
final Widget title;
final WidgetBuilder bodyBuilder;
final RefreshCallback onRefresh;
final bool loading;
final Widget Function(T payload) bodyBuilder;
final Future<T> Function() onRefresh;
final Widget trailing;
final List<Widget> actions;
final PreferredSizeWidget bottom;
@ -21,17 +19,51 @@ class RefreshScaffold extends StatelessWidget {
@required this.title,
@required this.bodyBuilder,
@required this.onRefresh,
@required this.loading,
this.trailing,
this.actions,
this.bottom,
});
@override
_RefreshScaffoldState createState() => _RefreshScaffoldState();
}
class _RefreshScaffoldState<T> extends State<RefreshScaffold<T>> {
bool loading;
T payload;
String error = '';
@override
void initState() {
super.initState();
_refresh();
}
Widget _buildBody() {
if (loading) {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: _refresh);
} else if (loading) {
return Loading(more: true);
} else {
return bodyBuilder();
return widget.bodyBuilder(payload);
}
}
Future<void> _refresh() async {
try {
setState(() {
error = '';
loading = true;
});
payload = await widget.onRefresh();
} catch (err) {
error = err.toString();
} finally {
if (mounted) {
setState(() {
loading = false;
});
}
}
}
@ -40,30 +72,27 @@ class RefreshScaffold extends StatelessWidget {
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoPageScaffold(
navigationBar:
CupertinoNavigationBar(middle: title, trailing: trailing),
navigationBar: CupertinoNavigationBar(
middle: widget.title, trailing: widget.trailing),
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverRefreshControl(onRefresh: onRefresh),
CupertinoSliverRefreshControl(onRefresh: _refresh),
SliverToBoxAdapter(child: _buildBody())
],
),
),
);
default:
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: title,
actions: actions,
bottom: bottom,
),
body: RefreshIndicator(
onRefresh: onRefresh,
child: SingleChildScrollView(child: _buildBody()),
),
return Scaffold(
appBar: AppBar(
title: widget.title,
actions: widget.actions,
bottom: widget.bottom,
),
body: RefreshIndicator(
onRefresh: _refresh,
child: SingleChildScrollView(child: _buildBody()),
),
);
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import '../providers/settings.dart';
import '../widgets/loading.dart';
import '../widgets/error_reload.dart';
// This is a scaffold for pull to refresh
class RefreshStatelessScaffold extends StatelessWidget {
final Widget title;
final Widget Function() bodyBuilder;
final Future<void> Function() onRefresh;
final bool loading;
final String error;
final Widget trailing;
final List<Widget> actions;
final PreferredSizeWidget bottom;
RefreshStatelessScaffold({
@required this.title,
@required this.bodyBuilder,
@required this.onRefresh,
@required this.loading,
@required this.error,
this.trailing,
this.actions,
this.bottom,
});
Widget _buildBody() {
if (error.isNotEmpty) {
return ErrorReload(text: error, reload: onRefresh);
} else if (loading) {
return Loading(more: true);
} else {
return bodyBuilder();
}
}
@override
Widget build(BuildContext context) {
switch (SettingsProvider.of(context).theme) {
case ThemeMap.cupertino:
return CupertinoPageScaffold(
navigationBar:
CupertinoNavigationBar(middle: title, trailing: trailing),
child: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverRefreshControl(onRefresh: onRefresh),
SliverToBoxAdapter(child: _buildBody())
],
),
),
);
default:
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: title,
actions: actions,
bottom: bottom,
),
body: RefreshIndicator(
onRefresh: onRefresh,
child: SingleChildScrollView(child: _buildBody()),
),
),
);
}
}
}

View File

@ -16,7 +16,7 @@ class _LoginScreenState extends State<LoginScreen> {
var settings = SettingsProvider.of(context);
return SimpleScaffold(
title: Text('Accounts'),
title: Text('Select account'),
bodyBuilder: () {
return Container(
child: Column(

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import '../scaffolds/refresh.dart';
import '../scaffolds/refresh_stateless.dart';
import '../providers/notification.dart';
import '../providers/settings.dart';
import '../widgets/notification_item.dart';
@ -31,6 +31,7 @@ class NotificationScreen extends StatefulWidget {
}
class NotificationScreenState extends State<NotificationScreen> {
String error = '';
int active = 0;
bool loading = true;
Map<String, NotificationGroup> groupMap = {};
@ -155,22 +156,23 @@ $key: pullRequest(number: ${item.number}) {
}
Future<void> _onSwitchTab([int index]) async {
if (index == null) {
index = active;
}
setState(() {
active = index;
error = '';
if (index != null) {
active = index;
}
loading = true;
});
var _groupMap = await fetchNotifications(active);
if (mounted) {
setState(() {
groupMap = _groupMap;
loading = false;
});
try {
groupMap = await fetchNotifications(active);
} catch (err) {
error = err.toString();
} finally {
if (mounted) {
setState(() {
loading = false;
});
}
}
}
@ -209,7 +211,7 @@ $key: pullRequest(number: ${item.number}) {
@override
Widget build(context) {
return RefreshScaffold(
return RefreshStatelessScaffold(
title: _buildTitle(),
bottom: TabBar(
onTap: _onSwitchTab,
@ -245,6 +247,7 @@ $key: pullRequest(number: ${item.number}) {
],
onRefresh: _onSwitchTab,
loading: loading,
error: error,
bodyBuilder: () {
return Column(
children: groupMap.entries

View File

@ -8,7 +8,6 @@ import '../widgets/repo_item.dart';
import '../widgets/entry_item.dart';
import '../screens/issues.dart';
import '../screens/pull_requests.dart';
import '../utils/utils.dart';
class RepoScreen extends StatefulWidget {
final String owner;
@ -21,16 +20,6 @@ class RepoScreen extends StatefulWidget {
}
class _RepoScreenState extends State<RepoScreen> {
Map<String, dynamic> payload;
String readme;
bool loading = true;
@override
void initState() {
super.initState();
nextTick(_refresh);
}
Future queryRepo(BuildContext context) async {
var owner = widget.owner;
var name = widget.name;
@ -77,30 +66,18 @@ class _RepoScreenState extends State<RepoScreen> {
return str;
}
Future<void> _refresh() async {
setState(() {
loading = true;
});
List items = await Future.wait([
queryRepo(context),
fetchReadme(context),
]);
if (mounted) {
setState(() {
loading = false;
payload = items[0];
readme = items[1];
});
}
}
@override
Widget build(BuildContext context) {
return RefreshScaffold(
loading: loading,
title: Text(widget.owner + '/' + widget.name),
onRefresh: _refresh,
bodyBuilder: () {
onRefresh: () => Future.wait([
queryRepo(context),
fetchReadme(context),
]),
bodyBuilder: (data) {
var payload = data[0];
var readme = data[1];
return Column(
children: <Widget>[
RepoItem(payload),

View File

@ -43,15 +43,6 @@ class UserScreen extends StatefulWidget {
}
class _UserScreenState extends State<UserScreen> {
bool loading = true;
Map<String, dynamic> payload = {};
@override
void initState() {
super.initState();
nextTick(_refresh);
}
Future queryUser(BuildContext context) async {
var login = widget.login;
var data = await SettingsProvider.of(context).query('''
@ -89,7 +80,7 @@ class _UserScreenState extends State<UserScreen> {
return data['user'];
}
Widget _buildRepos() {
Widget _buildRepos(payload) {
String title;
List items;
if (payload['pinnedRepositories']['nodes'].length == 0) {
@ -107,20 +98,7 @@ class _UserScreenState extends State<UserScreen> {
);
}
Future<void> _refresh() async {
setState(() {
loading = true;
});
var _payload = await queryUser(context);
if (mounted) {
setState(() {
loading = false;
payload = _payload;
});
}
}
Widget _buildEmail() {
Widget _buildEmail(payload) {
// TODO: redesign the UI to show all information
String email = payload['email'] ?? '';
if (email.isNotEmpty) {
@ -172,7 +150,7 @@ class _UserScreenState extends State<UserScreen> {
@override
Widget build(BuildContext context) {
return RefreshScaffold(
onRefresh: _refresh,
onRefresh: () => queryUser(context),
title: Text(widget.login),
trailing: Link(
child: Icon(Icons.settings, size: 24),
@ -180,8 +158,7 @@ class _UserScreenState extends State<UserScreen> {
material: false,
fullscreenDialog: true,
),
loading: loading,
bodyBuilder: () {
bodyBuilder: (payload) {
return Column(
children: <Widget>[
Container(
@ -202,7 +179,7 @@ class _UserScreenState extends State<UserScreen> {
Text(payload['name'] ?? widget.login,
style: TextStyle(height: 1.2)),
Padding(padding: EdgeInsets.only(top: 10)),
_buildEmail(),
_buildEmail(payload),
],
),
)
@ -241,7 +218,7 @@ class _UserScreenState extends State<UserScreen> {
],
),
),
_buildRepos(),
_buildRepos(payload),
],
);
},

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'link.dart';
class ErrorReload extends StatelessWidget {
final String text;
final Function reload;
ErrorReload({@required this.text, @required this.reload});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Column(
children: <Widget>[
Text(
'Woops, something bad happened. Error message:',
style: TextStyle(fontSize: 16),
),
Padding(padding: EdgeInsets.only(top: 10)),
Text(
text,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w300,
color: Colors.redAccent,
),
),
Padding(padding: EdgeInsets.only(top: 10)),
Link(
child: Text(
'Reload',
style: TextStyle(fontSize: 20, color: Colors.blueAccent),
),
beforeRedirect: reload,
material: false,
),
],
),
);
}
}