separate BLoC and widget pattern, known bug that graphql error is not return
ADD
example/lib/config.dart
+const String YOUR_PERSONAL_ACCESS_TOKEN = '';

RENAME
example/lib/bloc.dart
example/lib/graphql_bloc/bloc.dart
import 'package:rxdart/subjects.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
-import './mutations/mutations.dart' as mutations;
-import './queries/readRepositories.dart' as queries;
-
-
-const String YOUR_PERSONAL_ACCESS_TOKEN =
- '';
+import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+import '../graphql_operation/mutations/mutations.dart' as mutations;
+import '../graphql_operation/queries/readRepositories.dart' as queries;
class Repo {
const Repo({this.id, this.name, this.viewerHasStarred});

ADD
example/lib/graphql_bloc/main.dart
+import 'package:flutter/material.dart';
+
+import 'bloc.dart' show Bloc, Repo;
+
+class GraphQLBlocPatternScreen extends StatefulWidget {
+ GraphQLBlocPatternScreen({
+ Key key,
+ this.title = 'GraphQL Widget',
+ }) : bloc = Bloc(),
+ super(key: key);
+
+ final String title;
+ final Bloc bloc;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState(bloc);
+}
+
+class _MyHomePageState extends State<GraphQLBlocPatternScreen> {
+ _MyHomePageState(this.bloc);
+ final Bloc bloc;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: <Widget>[
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Number of repositories (default 50)',
+ ),
+ keyboardType: TextInputType.number,
+ onChanged: (String n) =>
+ bloc.updateNumberOfRepoSink.add(int.parse(n)),
+ ),
+ StreamBuilder<List<Repo>>(
+ stream: bloc.repoStream,
+ builder: (BuildContext context,
+ AsyncSnapshot<List<Repo>> snapshot) {
+ if (snapshot.hasError) {
+ return Text('\nErrors: \n ' +
+ (snapshot.error as List<dynamic>).join(',\n '));
+ }
+ if (snapshot.data == null) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ final List<Repo> repositories = snapshot.data;
+
+ return Expanded(
+ child: ListView.builder(
+ itemCount: repositories.length,
+ itemBuilder: (BuildContext context, int index) =>
+ StarrableRepository(
+ repository: repositories[index], bloc: bloc),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class StarrableRepository extends StatelessWidget {
+ const StarrableRepository({
+ Key key,
+ @required this.repository,
+ @required this.bloc,
+ }) : super(key: key);
+
+ final Bloc bloc;
+ final Repo repository;
+
+ Map<String, Object> extractRepositoryData(Map<String, Object> data) {
+ final Map<String, Object> action = data['action'] as Map<String, Object>;
+
+ if (action == null) {
+ return null;
+ }
+
+ return action['starrable'] as Map<String, Object>;
+ }
+
+ bool get viewerHasStarred => repository.viewerHasStarred;
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamBuilder<bool>(
+ stream: bloc.toggleStarLoadingStream,
+ initialData: false,
+ builder: (BuildContext context, AsyncSnapshot<bool> result) {
+ final bool loading = result.data;
+ return ListTile(
+ leading: viewerHasStarred
+ ? const Icon(
+ Icons.star,
+ color: Colors.amber,
+ )
+ : const Icon(Icons.star_border),
+ trailing: loading ? const CircularProgressIndicator() : null,
+ title: Text(repository.name),
+ onTap: () {
+ bloc.toggleStarSink.add(repository);
+ },
+ );
+ },
+ );
+ }
+}

RENAME
example/lib/mutations/addStar.dart
example/lib/graphql_operation/mutations/addStar.dart
RENAME
example/lib/mutations/mutations.dart
example/lib/graphql_operation/mutations/mutations.dart
RENAME
example/lib/mutations/removeStar.dart
example/lib/graphql_operation/mutations/removeStar.dart
RENAME
example/lib/queries/readRepositories.dart
example/lib/graphql_operation/queries/readRepositories.dart
ADD
example/lib/graphql_widget/main.dart
+import 'package:flutter/material.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+import '../graphql_operation/mutations/mutations.dart' as mutations;
+import '../graphql_operation/queries/readRepositories.dart' as queries;
+
+class GraphQLWidgetScreen extends StatelessWidget {
+ const GraphQLWidgetScreen(): super();
+ @override
+ Widget build(BuildContext context) {
+ final HttpLink httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql',
+ );
+
+ final AuthLink authLink = AuthLink(
+ getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ );
+
+ final Link link = authLink.concat(httpLink as Link);
+
+ final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
+ GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: link,
+ ),
+ );
+
+ return GraphQLProvider(
+ client: client,
+ child: const CacheProvider(
+ child: MyHomePage(title: 'GraphQL Widget'),
+ ),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({
+ Key key,
+ this.title,
+ }) : super(key: key);
+
+ final String title;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ int nRepositories = 50;
+
+ void changeQuery(String number) {
+ setState(() {
+ nRepositories = int.parse(number) ?? 50;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: <Widget>[
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Number of repositories (default 50)',
+ ),
+ keyboardType: TextInputType.number,
+ onSubmitted: changeQuery,
+ ),
+ Query(
+ options: QueryOptions(
+ document: queries.readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': nRepositories,
+ },
+ pollInterval: 4,
+ ),
+ builder: (QueryResult result) {
+ if (result.loading) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ if (result.hasErrors) {
+ return Text('\nErrors: \n ' + result.errors.join(',\n '));
+ }
+
+ if (result.data == null && result.errors == null) {
+ return const Text('Both data and errors are null, this is a known bug after refactoring, you might forget to set Github token');
+ }
+
+ // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
+ final List<dynamic> repositories = result.data['viewer']
+ ['repositories']['nodes'] as List<dynamic>;
+
+ return Expanded(
+ child: ListView.builder(
+ itemCount: repositories.length,
+ itemBuilder: (BuildContext context, int index) =>
+ StarrableRepository(
+ repository:
+ repositories[index] as Map<String, Object>),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class StarrableRepository extends StatefulWidget {
+ const StarrableRepository({
+ Key key,
+ @required this.repository,
+ }) : super(key: key);
+
+ final Map<String, Object> repository;
+
+ @override
+ StarrableRepositoryState createState() {
+ return StarrableRepositoryState();
+ }
+}
+
+class StarrableRepositoryState extends State<StarrableRepository> {
+ bool loading = false;
+
+ Map<String, Object> extractRepositoryData(Map<String, Object> data) {
+ final Map<String, Object> action = data['action'] as Map<String, Object>;
+
+ if (action == null) {
+ return null;
+ }
+
+ return action['starrable'] as Map<String, Object>;
+ }
+
+ bool get viewerHasStarred => widget.repository['viewerHasStarred'] as bool;
+
+ @override
+ Widget build(BuildContext context) {
+ final bool starred = loading ? !viewerHasStarred : viewerHasStarred;
+
+ return Mutation(
+ key: Key(starred.toString()),
+ options: MutationOptions(
+ document: starred ? mutations.removeStar : mutations.addStar,
+ ),
+ builder: (RunMutation toggleStar, QueryResult result) {
+ return ListTile(
+ leading: starred
+ ? const Icon(
+ Icons.star,
+ color: Colors.amber,
+ )
+ : const Icon(Icons.star_border),
+ trailing: loading ? const CircularProgressIndicator() : null,
+ title: Text(widget.repository['name'] as String),
+ onTap: () {
+ // optimistic ui updates are not implemented yet,
+ // so we track loading manually
+ setState(() {
+ loading = true;
+ });
+ toggleStar(<String, dynamic>{
+ 'starrableId': widget.repository['id'],
+ });
+ },
+ );
+ },
+ update: (Cache cache, QueryResult result) {
+ if (result.hasErrors) {
+ print(result.errors);
+ } else {
+ final Map<String, Object> updated = Map<String, Object>.from(
+ widget.repository)
+ ..addAll(extractRepositoryData(result.data as Map<String, Object>));
+
+ cache.write(typenameDataIdFromObject(updated), updated);
+ }
+ },
+ onCompleted: (QueryResult result) {
+ showDialog<AlertDialog>(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ extractRepositoryData(result.data as Map<String, Object>)[
+ 'viewerHasStarred'] as bool
+ ? 'Thanks for your star!'
+ : 'Sorry you changed your mind!',
+ ),
+ actions: <Widget>[
+ SimpleDialogOption(
+ child: const Text('Dismiss'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ },
+ );
+ setState(() {
+ loading = false;
+ });
+ },
+ );
+ }
+}

MODIFY
example/lib/main.dart
example/lib/main.dart
import 'package:flutter/material.dart';
+import './graphql_bloc/main.dart' show GraphQLBlocPatternScreen;
+import './graphql_widget/main.dart' show GraphQLWidgetScreen;
-import 'bloc.dart' show Bloc, Repo;
-
-void main() => runApp(MyApp());
-
-class MyApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
+void main() => runApp(
+ MaterialApp(
title: 'GraphQL Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
- home: MyHomePage(title: 'GraphQL Flutter Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- MyHomePage({
- Key key,
- this.title,
- }) : bloc = Bloc(),
- super(key: key);
-
- final String title;
- final Bloc bloc;
-
- @override
- _MyHomePageState createState() => _MyHomePageState(bloc);
-}
-
-class _MyHomePageState extends State<MyHomePage> {
- _MyHomePageState(this.bloc);
- final Bloc bloc;
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
+ home: Builder(
+ builder: (BuildContext context) => Scaffold(
appBar: AppBar(
- title: Text(widget.title),
+ title: const Text('GraphQL Demo App'),
),
- body: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ body: Center(
child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- mainAxisSize: MainAxisSize.max,
children: <Widget>[
- TextField(
- decoration: const InputDecoration(
- labelText: 'Number of repositories (default 50)',
- ),
- keyboardType: TextInputType.number,
- onChanged: (String n) =>
- bloc.updateNumberOfRepoSink.add(int.parse(n)),
+ RaisedButton(
+ child: const Text('GraphQL BloC pattern'),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute<GraphQLWidgetScreen>(
+ builder: (BuildContext context) =>
+ GraphQLBlocPatternScreen(),
),
- StreamBuilder<List<Repo>>(
- stream: bloc.repoStream,
- builder: (BuildContext context,
- AsyncSnapshot<List<Repo>> snapshot) {
- if (snapshot.hasError) {
- return Text('\nErrors: \n ' +
- (snapshot.error as List<dynamic>).join(',\n '));
- }
- if (snapshot.data == null) {
- return const Center(
- child: CircularProgressIndicator(),
);
- }
-
- final List<Repo> repositories = snapshot.data;
-
- return Expanded(
- child: ListView.builder(
- itemCount: repositories.length,
- itemBuilder: (BuildContext context, int index) =>
- StarrableRepository(
- repository: repositories[index], bloc: bloc),
+ },
+ ),
+ RaisedButton(
+ child: const Text('GraphQL Widget'),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute<GraphQLWidgetScreen>(
+ builder: (BuildContext context) =>
+ const GraphQLWidgetScreen(),
),
);
},
@@ -84,53 +43,7 @@ class _MyHomePageState extends State<MyHomePage> {
],
),
),
+ ),
+ ),
+ ),
);
- }
-}
-
-class StarrableRepository extends StatelessWidget {
- const StarrableRepository({
- Key key,
- @required this.repository,
- @required this.bloc,
- }) : super(key: key);
-
- final Bloc bloc;
- final Repo repository;
-
- Map<String, Object> extractRepositoryData(Map<String, Object> data) {
- final Map<String, Object> action = data['action'] as Map<String, Object>;
-
- if (action == null) {
- return null;
- }
-
- return action['starrable'] as Map<String, Object>;
- }
-
- bool get viewerHasStarred => repository.viewerHasStarred;
-
- @override
- Widget build(BuildContext context) {
- return StreamBuilder<bool>(
- stream: bloc.toggleStarLoadingStream,
- initialData: false,
- builder: (BuildContext context, AsyncSnapshot<bool> result) {
- final bool loading = result.data;
- return ListTile(
- leading: viewerHasStarred
- ? const Icon(
- Icons.star,
- color: Colors.amber,
- )
- : const Icon(Icons.star_border),
- trailing: loading ? const CircularProgressIndicator() : null,
- title: Text(repository.name),
- onTap: () {
- bloc.toggleStarSink.add(repository);
- },
- );
- },
- );
- }
-}

separate BLoC and widget pattern, known bug that graphql error is not return (#4)