Skip to content

Commit 295512d

Browse files
truongsinhmicimize
authored andcommitted
separate BLoC and widget pattern, known bug that graphql error is not return (#4)
1 parent b68b734 commit 295512d

File tree

9 files changed

+393
-137
lines changed

9 files changed

+393
-137
lines changed

example/lib/config.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const String YOUR_PERSONAL_ACCESS_TOKEN = '';
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import 'package:rxdart/subjects.dart';
22
import 'package:graphql_flutter/graphql_flutter.dart';
33

4-
import './mutations/mutations.dart' as mutations;
5-
import './queries/readRepositories.dart' as queries;
6-
7-
8-
const String YOUR_PERSONAL_ACCESS_TOKEN =
9-
'';
4+
import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
5+
import '../graphql_operation/mutations/mutations.dart' as mutations;
6+
import '../graphql_operation/queries/readRepositories.dart' as queries;
107

118
class Repo {
129
const Repo({this.id, this.name, this.viewerHasStarred});

example/lib/graphql_bloc/main.dart

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'bloc.dart' show Bloc, Repo;
4+
5+
class GraphQLBlocPatternScreen extends StatefulWidget {
6+
GraphQLBlocPatternScreen({
7+
Key key,
8+
this.title = 'GraphQL Widget',
9+
}) : bloc = Bloc(),
10+
super(key: key);
11+
12+
final String title;
13+
final Bloc bloc;
14+
15+
@override
16+
_MyHomePageState createState() => _MyHomePageState(bloc);
17+
}
18+
19+
class _MyHomePageState extends State<GraphQLBlocPatternScreen> {
20+
_MyHomePageState(this.bloc);
21+
final Bloc bloc;
22+
23+
@override
24+
Widget build(BuildContext context) {
25+
return Scaffold(
26+
appBar: AppBar(
27+
title: Text(widget.title),
28+
),
29+
body: Container(
30+
padding: const EdgeInsets.symmetric(horizontal: 8.0),
31+
child: Column(
32+
mainAxisAlignment: MainAxisAlignment.start,
33+
mainAxisSize: MainAxisSize.max,
34+
children: <Widget>[
35+
TextField(
36+
decoration: const InputDecoration(
37+
labelText: 'Number of repositories (default 50)',
38+
),
39+
keyboardType: TextInputType.number,
40+
onChanged: (String n) =>
41+
bloc.updateNumberOfRepoSink.add(int.parse(n)),
42+
),
43+
StreamBuilder<List<Repo>>(
44+
stream: bloc.repoStream,
45+
builder: (BuildContext context,
46+
AsyncSnapshot<List<Repo>> snapshot) {
47+
if (snapshot.hasError) {
48+
return Text('\nErrors: \n ' +
49+
(snapshot.error as List<dynamic>).join(',\n '));
50+
}
51+
if (snapshot.data == null) {
52+
return const Center(
53+
child: CircularProgressIndicator(),
54+
);
55+
}
56+
57+
final List<Repo> repositories = snapshot.data;
58+
59+
return Expanded(
60+
child: ListView.builder(
61+
itemCount: repositories.length,
62+
itemBuilder: (BuildContext context, int index) =>
63+
StarrableRepository(
64+
repository: repositories[index], bloc: bloc),
65+
),
66+
);
67+
},
68+
),
69+
],
70+
),
71+
),
72+
);
73+
}
74+
}
75+
76+
class StarrableRepository extends StatelessWidget {
77+
const StarrableRepository({
78+
Key key,
79+
@required this.repository,
80+
@required this.bloc,
81+
}) : super(key: key);
82+
83+
final Bloc bloc;
84+
final Repo repository;
85+
86+
Map<String, Object> extractRepositoryData(Map<String, Object> data) {
87+
final Map<String, Object> action = data['action'] as Map<String, Object>;
88+
89+
if (action == null) {
90+
return null;
91+
}
92+
93+
return action['starrable'] as Map<String, Object>;
94+
}
95+
96+
bool get viewerHasStarred => repository.viewerHasStarred;
97+
98+
@override
99+
Widget build(BuildContext context) {
100+
return StreamBuilder<bool>(
101+
stream: bloc.toggleStarLoadingStream,
102+
initialData: false,
103+
builder: (BuildContext context, AsyncSnapshot<bool> result) {
104+
final bool loading = result.data;
105+
return ListTile(
106+
leading: viewerHasStarred
107+
? const Icon(
108+
Icons.star,
109+
color: Colors.amber,
110+
)
111+
: const Icon(Icons.star_border),
112+
trailing: loading ? const CircularProgressIndicator() : null,
113+
title: Text(repository.name),
114+
onTap: () {
115+
bloc.toggleStarSink.add(repository);
116+
},
117+
);
118+
},
119+
);
120+
}
121+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:graphql_flutter/graphql_flutter.dart';
3+
4+
import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
5+
import '../graphql_operation/mutations/mutations.dart' as mutations;
6+
import '../graphql_operation/queries/readRepositories.dart' as queries;
7+
8+
class GraphQLWidgetScreen extends StatelessWidget {
9+
const GraphQLWidgetScreen(): super();
10+
@override
11+
Widget build(BuildContext context) {
12+
final HttpLink httpLink = HttpLink(
13+
uri: 'https://api.github.com/graphql',
14+
);
15+
16+
final AuthLink authLink = AuthLink(
17+
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
18+
);
19+
20+
final Link link = authLink.concat(httpLink as Link);
21+
22+
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
23+
GraphQLClient(
24+
cache: NormalizedInMemoryCache(
25+
dataIdFromObject: typenameDataIdFromObject,
26+
),
27+
link: link,
28+
),
29+
);
30+
31+
return GraphQLProvider(
32+
client: client,
33+
child: const CacheProvider(
34+
child: MyHomePage(title: 'GraphQL Widget'),
35+
),
36+
);
37+
}
38+
}
39+
40+
class MyHomePage extends StatefulWidget {
41+
const MyHomePage({
42+
Key key,
43+
this.title,
44+
}) : super(key: key);
45+
46+
final String title;
47+
48+
@override
49+
_MyHomePageState createState() => _MyHomePageState();
50+
}
51+
52+
class _MyHomePageState extends State<MyHomePage> {
53+
int nRepositories = 50;
54+
55+
void changeQuery(String number) {
56+
setState(() {
57+
nRepositories = int.parse(number) ?? 50;
58+
});
59+
}
60+
61+
@override
62+
Widget build(BuildContext context) {
63+
return Scaffold(
64+
appBar: AppBar(
65+
title: Text(widget.title),
66+
),
67+
body: Container(
68+
padding: const EdgeInsets.symmetric(horizontal: 8.0),
69+
child: Column(
70+
mainAxisAlignment: MainAxisAlignment.start,
71+
mainAxisSize: MainAxisSize.max,
72+
children: <Widget>[
73+
TextField(
74+
decoration: const InputDecoration(
75+
labelText: 'Number of repositories (default 50)',
76+
),
77+
keyboardType: TextInputType.number,
78+
onSubmitted: changeQuery,
79+
),
80+
Query(
81+
options: QueryOptions(
82+
document: queries.readRepositories,
83+
variables: <String, dynamic>{
84+
'nRepositories': nRepositories,
85+
},
86+
pollInterval: 4,
87+
),
88+
builder: (QueryResult result) {
89+
if (result.loading) {
90+
return const Center(
91+
child: CircularProgressIndicator(),
92+
);
93+
}
94+
95+
if (result.hasErrors) {
96+
return Text('\nErrors: \n ' + result.errors.join(',\n '));
97+
}
98+
99+
if (result.data == null && result.errors == null) {
100+
return const Text('Both data and errors are null, this is a known bug after refactoring, you might forget to set Github token');
101+
}
102+
103+
// result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
104+
final List<dynamic> repositories = result.data['viewer']
105+
['repositories']['nodes'] as List<dynamic>;
106+
107+
return Expanded(
108+
child: ListView.builder(
109+
itemCount: repositories.length,
110+
itemBuilder: (BuildContext context, int index) =>
111+
StarrableRepository(
112+
repository:
113+
repositories[index] as Map<String, Object>),
114+
),
115+
);
116+
},
117+
),
118+
],
119+
),
120+
),
121+
);
122+
}
123+
}
124+
125+
class StarrableRepository extends StatefulWidget {
126+
const StarrableRepository({
127+
Key key,
128+
@required this.repository,
129+
}) : super(key: key);
130+
131+
final Map<String, Object> repository;
132+
133+
@override
134+
StarrableRepositoryState createState() {
135+
return StarrableRepositoryState();
136+
}
137+
}
138+
139+
class StarrableRepositoryState extends State<StarrableRepository> {
140+
bool loading = false;
141+
142+
Map<String, Object> extractRepositoryData(Map<String, Object> data) {
143+
final Map<String, Object> action = data['action'] as Map<String, Object>;
144+
145+
if (action == null) {
146+
return null;
147+
}
148+
149+
return action['starrable'] as Map<String, Object>;
150+
}
151+
152+
bool get viewerHasStarred => widget.repository['viewerHasStarred'] as bool;
153+
154+
@override
155+
Widget build(BuildContext context) {
156+
final bool starred = loading ? !viewerHasStarred : viewerHasStarred;
157+
158+
return Mutation(
159+
key: Key(starred.toString()),
160+
options: MutationOptions(
161+
document: starred ? mutations.removeStar : mutations.addStar,
162+
),
163+
builder: (RunMutation toggleStar, QueryResult result) {
164+
return ListTile(
165+
leading: starred
166+
? const Icon(
167+
Icons.star,
168+
color: Colors.amber,
169+
)
170+
: const Icon(Icons.star_border),
171+
trailing: loading ? const CircularProgressIndicator() : null,
172+
title: Text(widget.repository['name'] as String),
173+
onTap: () {
174+
// optimistic ui updates are not implemented yet,
175+
// so we track loading manually
176+
setState(() {
177+
loading = true;
178+
});
179+
toggleStar(<String, dynamic>{
180+
'starrableId': widget.repository['id'],
181+
});
182+
},
183+
);
184+
},
185+
update: (Cache cache, QueryResult result) {
186+
if (result.hasErrors) {
187+
print(result.errors);
188+
} else {
189+
final Map<String, Object> updated = Map<String, Object>.from(
190+
widget.repository)
191+
..addAll(extractRepositoryData(result.data as Map<String, Object>));
192+
193+
cache.write(typenameDataIdFromObject(updated), updated);
194+
}
195+
},
196+
onCompleted: (QueryResult result) {
197+
showDialog<AlertDialog>(
198+
context: context,
199+
builder: (BuildContext context) {
200+
return AlertDialog(
201+
title: Text(
202+
extractRepositoryData(result.data as Map<String, Object>)[
203+
'viewerHasStarred'] as bool
204+
? 'Thanks for your star!'
205+
: 'Sorry you changed your mind!',
206+
),
207+
actions: <Widget>[
208+
SimpleDialogOption(
209+
child: const Text('Dismiss'),
210+
onPressed: () {
211+
Navigator.of(context).pop();
212+
},
213+
)
214+
],
215+
);
216+
},
217+
);
218+
setState(() {
219+
loading = false;
220+
});
221+
},
222+
);
223+
}
224+
}

0 commit comments

Comments
 (0)