Skip to content

Commit 8342d24

Browse files
fabis94vincenzopalazzo
authored andcommitted
feat: added query onError & onComplete callbacks
1 parent ff6fda6 commit 8342d24

File tree

4 files changed

+151
-10
lines changed

4 files changed

+151
-10
lines changed

packages/graphql/lib/src/core/observable_query.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class ObservableQuery<TParsed> {
8787
/// callbacks registered with [onData]
8888
List<OnData<TParsed>> _onDataCallbacks = [];
8989

90+
/// same as [_onDataCallbacks], but not removed after invocation
91+
Set<OnData<TParsed>> _notRemovableOnDataCallbacks = Set();
92+
9093
/// call [queryManager.maybeRebroadcastQueries] after all other [_onDataCallbacks]
9194
///
9295
/// Automatically appended as an [OnData]
@@ -273,9 +276,24 @@ class ObservableQuery<TParsed> {
273276
/// result that [QueryResult.isConcrete],
274277
/// handling the resolution of [lifecycle] from
275278
/// [QueryLifecycle.sideEffectsBlocking] to [QueryLifecycle.completed]
276-
/// as appropriate
277-
void onData(Iterable<OnData<TParsed>> callbacks) =>
278-
_onDataCallbacks.addAll(callbacks);
279+
/// as appropriate, unless if [removeAfterInvocation] is set to false.
280+
///
281+
/// Returns a function for removing the added callbacks
282+
void Function() onData(
283+
Iterable<OnData<TParsed>> callbacks, {
284+
bool removeAfterInvocation = true,
285+
}) {
286+
_onDataCallbacks.addAll(callbacks);
287+
288+
if (!removeAfterInvocation) {
289+
_notRemovableOnDataCallbacks.addAll(callbacks);
290+
}
291+
292+
return () {
293+
_onDataCallbacks.removeWhere((cb) => callbacks.contains(cb));
294+
_notRemovableOnDataCallbacks.removeWhere((cb) => callbacks.contains(cb));
295+
};
296+
}
279297

280298
/// Applies [onData] callbacks at the end of [addResult]
281299
///
@@ -300,7 +318,8 @@ class ObservableQuery<TParsed> {
300318

301319
if (result!.isConcrete) {
302320
// avoid removing new callbacks
303-
_onDataCallbacks.removeWhere((cb) => callbacks.contains(cb));
321+
_onDataCallbacks.removeWhere((cb) =>
322+
callbacks.contains(cb) && !_notRemovableOnDataCallbacks.contains(cb));
304323

305324
// if there are new callbacks, there is maybe another inflight mutation
306325
if (_onDataCallbacks.isEmpty) {

packages/graphql/lib/src/core/query_options.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:graphql/src/core/_base_options.dart';
24
import 'package:graphql/src/core/result_parser.dart';
35
import 'package:graphql/src/utilities/helpers.dart';
@@ -7,6 +9,10 @@ import 'package:gql/ast.dart';
79
import 'package:graphql/client.dart';
810
import 'package:meta/meta.dart';
911

12+
typedef OnQueryComplete = FutureOr<void> Function(Map<String, dynamic> data);
13+
14+
typedef OnQueryError = FutureOr<void> Function(OperationException? error);
15+
1016
/// Query options.
1117
@immutable
1218
class QueryOptions<TParsed extends Object?> extends BaseOptions<TParsed> {
@@ -21,6 +27,8 @@ class QueryOptions<TParsed extends Object?> extends BaseOptions<TParsed> {
2127
this.pollInterval,
2228
Context? context,
2329
ResultParserFn<TParsed>? parserFn,
30+
this.onComplete,
31+
this.onError,
2432
}) : super(
2533
fetchPolicy: fetchPolicy,
2634
errorPolicy: errorPolicy,
@@ -33,13 +41,18 @@ class QueryOptions<TParsed extends Object?> extends BaseOptions<TParsed> {
3341
parserFn: parserFn,
3442
);
3543

44+
final OnQueryComplete? onComplete;
45+
final OnQueryError? onError;
46+
3647
/// The time interval on which this query should be re-fetched from the server.
3748
final Duration? pollInterval;
3849

3950
@override
4051
List<Object?> get properties => [
4152
...super.properties,
4253
pollInterval,
54+
onComplete,
55+
onError,
4356
];
4457

4558
QueryOptions<TParsed> withFetchMoreOptions(
@@ -335,3 +348,39 @@ extension WithType on Request {
335348
bool get isMutation => type == OperationType.mutation;
336349
bool get isSubscription => type == OperationType.subscription;
337350
}
351+
352+
/// Handles execution of query callbacks
353+
class QueryCallbackHandler<TParsed> {
354+
final QueryOptions<TParsed> options;
355+
356+
QueryCallbackHandler({required this.options});
357+
358+
Iterable<OnData<TParsed>> get callbacks {
359+
var callbacks = List<OnData<TParsed>?>.empty(growable: true);
360+
callbacks.addAll([onCompleted, onError]);
361+
// FIXME: can we remove the type in whereType?
362+
return callbacks.whereType<OnData<TParsed>>();
363+
}
364+
365+
OnData<TParsed>? get onCompleted {
366+
if (options.onComplete != null) {
367+
return (QueryResult? result) {
368+
if (!result!.isLoading && !result.isOptimistic) {
369+
return options.onComplete!(result.data ?? {});
370+
}
371+
};
372+
}
373+
return null;
374+
}
375+
376+
OnData<TParsed>? get onError {
377+
if (options.onError != null && options.errorPolicy != ErrorPolicy.ignore) {
378+
return (QueryResult? result) {
379+
if (!result!.isLoading && result.hasException) {
380+
return options.onError!(result.exception);
381+
}
382+
};
383+
}
384+
return null;
385+
}
386+
}

packages/graphql_flutter/lib/src/widgets/hooks/query.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ QueryHookResult<TParsed> useQueryOnClient<TParsed>(
4040
query.stream,
4141
initialData: query.latestResult,
4242
);
43+
44+
useEffect(() {
45+
final cleanup = query.onData(
46+
QueryCallbackHandler(options: options).callbacks,
47+
removeAfterInvocation: false,
48+
);
49+
return cleanup;
50+
}, [options, query]);
51+
4352
return QueryHookResult(
4453
result: snapshot.data!,
4554
refetch: query.refetch,

packages/graphql_flutter/test/widgets/query_test.dart

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:convert';
12
import 'dart:io';
23

34
import 'package:flutter/material.dart';
@@ -52,12 +53,20 @@ class Page extends StatefulWidget {
5253
final FetchPolicy? fetchPolicy;
5354
final ErrorPolicy? errorPolicy;
5455

55-
Page({
56-
Key? key,
57-
this.variables,
58-
this.fetchPolicy,
59-
this.errorPolicy,
60-
}) : super(key: key);
56+
/// Query onComplete callback
57+
final OnQueryComplete? onQueryComplete;
58+
59+
/// Query onError callbacks
60+
final OnQueryError? onQueryError;
61+
62+
Page(
63+
{Key? key,
64+
this.variables,
65+
this.fetchPolicy,
66+
this.errorPolicy,
67+
this.onQueryComplete,
68+
this.onQueryError})
69+
: super(key: key);
6170

6271
@override
6372
State<StatefulWidget> createState() => PageState();
@@ -102,6 +111,8 @@ class PageState extends State<Page> {
102111
variables: variables,
103112
fetchPolicy: fetchPolicy,
104113
errorPolicy: errorPolicy,
114+
onComplete: widget.onQueryComplete,
115+
onError: widget.onQueryError,
105116
),
106117
builder: (result, {Refetch? refetch, FetchMore? fetchMore}) =>
107118
Container(),
@@ -370,5 +381,58 @@ void main() {
370381
await tester.pump();
371382
verifyNoMoreInteractions(mockHttpClient);
372383
});
384+
385+
testWidgets('invokes onComplete callback once when completed',
386+
(WidgetTester tester) async {
387+
var onCompleteHits = 0;
388+
final page = Page(
389+
variables: {'foo': 1},
390+
onQueryComplete: (a) {
391+
onCompleteHits++;
392+
},
393+
);
394+
395+
await tester.pumpWidget(GraphQLProvider(
396+
client: client,
397+
child: page,
398+
));
399+
400+
expect(onCompleteHits, equals(1));
401+
});
402+
403+
testWidgets('invokes onError callback once when error thrown',
404+
(WidgetTester tester) async {
405+
var errorMsg = "Something bad has happened!";
406+
List<GraphQLError> errors = [];
407+
408+
final page = Page(
409+
variables: {'foo': 1},
410+
onQueryError: (a) {
411+
errors.addAll(a?.graphqlErrors ?? []);
412+
},
413+
);
414+
415+
when(mockHttpClient.send(captureAny))
416+
.thenAnswer((_) => Future<http.StreamedResponse>.value(
417+
http.StreamedResponse(
418+
Stream.value(utf8.encode(jsonEncode({
419+
"data": null,
420+
"errors": [
421+
{"message": errorMsg}
422+
],
423+
}))),
424+
200,
425+
),
426+
));
427+
428+
await tester.pumpWidget(GraphQLProvider(
429+
client: client,
430+
child: page,
431+
));
432+
433+
expect(errors, isNotEmpty);
434+
expect(errors.map((e) => e.message).join(), contains(errorMsg));
435+
expect(errors, hasLength(1));
436+
});
373437
});
374438
}

0 commit comments

Comments
 (0)