diff --git a/packages/example/lib/components/basic_mutation.dart b/packages/example/lib/components/basic_mutation.dart index 03c8078..01f39f0 100644 --- a/packages/example/lib/components/basic_mutation.dart +++ b/packages/example/lib/components/basic_mutation.dart @@ -56,12 +56,14 @@ class _BasicMutationExampleState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "# Basic Mutation Example", + "# Basic Mutation Example (with Failure & Retry simulation)", style: Theme.of(context).textTheme.headline5, ), MutationBuilder>( job: basicMutationJob, onMutate: (v) { + final data = + QueryBowl.of(context).getQuery(successJob.queryKey)?.data; QueryBowl.of(context) .setQueryData(successJob.queryKey, (oldData) { if (oldData?.contains("After Mutate (OPTIMISTIC UPDATE)") == @@ -70,6 +72,11 @@ class _BasicMutationExampleState extends State { } return "$oldData - After Mutate (OPTIMISTIC UPDATE)"; }); + return data; + }, + onData: (data, variables, context) { + print("Passed Variable: $variables"); + print("Safe Previous Value: $context"); }, builder: (context, mutation) { return Padding( @@ -94,7 +101,7 @@ class _BasicMutationExampleState extends State { "title": title, "body": body, "id": id, - }, onData: (data) { + }, onData: (data, variables, context) { // resetting the form titleController.text = ""; bodyController.text = ""; diff --git a/packages/example/lib/components/hooks/basic_hook_mutation.dart b/packages/example/lib/components/hooks/basic_hook_mutation.dart index 2d7ba3b..27165fe 100644 --- a/packages/example/lib/components/hooks/basic_hook_mutation.dart +++ b/packages/example/lib/components/hooks/basic_hook_mutation.dart @@ -49,7 +49,7 @@ class BasicHookMutationExample extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "# Basic Mutation Hook Example", + "# Basic Mutation Hook Example (with failure & retry simulation)", style: Theme.of(context).textTheme.headline5, ), Padding( @@ -74,7 +74,7 @@ class BasicHookMutationExample extends HookWidget { "title": title, "body": body, "id": id, - }, onData: (data) { + }, onData: (data, variable, context) { // resetting the form titleController.text = ""; bodyController.text = ""; diff --git a/packages/fl_query/.flutter-plugins-dependencies b/packages/fl_query/.flutter-plugins-dependencies index 2c1d65f..2ece47b 100644 --- a/packages/fl_query/.flutter-plugins-dependencies +++ b/packages/fl_query/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.5/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.5/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.2/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]}],"date_created":"2022-07-20 12:20:16.617434","version":"3.0.1"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.5/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.5/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.2/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]}],"date_created":"2022-07-28 10:55:33.933539","version":"3.0.1"} \ No newline at end of file diff --git a/packages/fl_query/lib/src/models/query_job.dart b/packages/fl_query/lib/src/models/query_job.dart index b746c32..7e1ffcd 100644 --- a/packages/fl_query/lib/src/models/query_job.dart +++ b/packages/fl_query/lib/src/models/query_job.dart @@ -1,3 +1,4 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fl_query/src/query.dart'; class QueryJob { @@ -20,6 +21,7 @@ class QueryJob { Duration? cacheTime; Duration? refetchInterval; + Connectivity? connectivity; QueryJob({ required String queryKey, @@ -34,6 +36,7 @@ class QueryJob { this.refetchOnMount, this.refetchOnReconnect, this.refetchOnExternalDataChange, + this.connectivity, }) : _queryKey = queryKey; String get queryKey => _queryKey; @@ -56,6 +59,7 @@ class QueryJob { bool? refetchOnMount, bool? refetchOnReconnect, bool? refetchOnExternalDataChange, + Connectivity? connectivity, }) { return (String queryKey) { if (preQueryKey != null) queryKey = "$preQueryKey#$queryKey"; @@ -72,6 +76,7 @@ class QueryJob { refetchOnMount: refetchOnMount, refetchOnReconnect: refetchOnReconnect, refetchOnExternalDataChange: refetchOnExternalDataChange, + connectivity: connectivity, ); }; } diff --git a/packages/fl_query/lib/src/mutation.dart b/packages/fl_query/lib/src/mutation.dart index f017827..d3f7a5b 100644 --- a/packages/fl_query/lib/src/mutation.dart +++ b/packages/fl_query/lib/src/mutation.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fl_query/fl_query.dart'; import 'package:fl_query/src/base_operation.dart'; import 'package:fl_query/src/models/mutation_job.dart'; import 'package:flutter/widgets.dart'; @@ -11,7 +12,13 @@ enum MutationStatus { idle, } -typedef MutationListener = FutureOr Function(T); +typedef MutationListenerReturnable = FutureOr Function(T); + +typedef MutationListener = FutureOr Function( + T payload, + V variables, + dynamic context, +); typedef MutationTaskFunction = FutureOr Function( String queryKey, V variables); @@ -23,12 +30,18 @@ class Mutation extends BaseOperation { MutationStatus status; + dynamic _sideEffectContext; + @protected - final Set> onDataListeners = {}; + final Set> _onDataListeners = {}; @protected - final Set> onErrorListeners = {}; + final Set> _onErrorListeners = {}; @protected - final Set> onMutateListeners = {}; + final Set> _onMutateListeners = {}; + + // using late as _variables will only be used after a [mutate] or + // [mutateAsync] is executed + late V _variables; Mutation({ required this.mutationKey, @@ -37,21 +50,21 @@ class Mutation extends BaseOperation { required super.retryDelay, required super.queryBowl, required Duration cacheTime, - MutationListener? onData, - MutationListener? onError, - MutationListener? onMutate, + MutationListener? onData, + MutationListener? onError, + MutationListenerReturnable? onMutate, }) : status = MutationStatus.idle, super(cacheTime: cacheTime) { - if (onData != null) onDataListeners.add(onData); - if (onError != null) onErrorListeners.add(onError); - if (onMutate != null) onMutateListeners.add(onMutate); + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); + if (onMutate != null) _onMutateListeners.add(onMutate); } Mutation.fromOptions( MutationJob options, { - MutationListener? onData, - MutationListener? onError, - MutationListener? onMutate, + MutationListener? onData, + MutationListener? onError, + MutationListenerReturnable? onMutate, required super.queryBowl, }) : mutationKey = options.mutationKey, task = options.task, @@ -61,8 +74,8 @@ class Mutation extends BaseOperation { retryDelay: options.retryDelay ?? const Duration(milliseconds: 200), cacheTime: options.cacheTime ?? const Duration(minutes: 5), ) { - if (onData != null) onDataListeners.add(onData); - if (onError != null) onErrorListeners.add(onError); + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); } // all methods @@ -74,22 +87,22 @@ class Mutation extends BaseOperation { status = MutationStatus.loading; notifyListeners(); retryAttempts = 0; - for (final onMutate in onMutateListeners) { - onMutate(variables); + for (final onMutate in _onMutateListeners) { + _sideEffectContext = await onMutate(variables); } data = await task(mutationKey, variables); updatedAt = DateTime.now(); status = MutationStatus.success; - for (final onData in onDataListeners) { - onData(data!); + for (final onData in _onDataListeners) { + onData(data!, _variables, _sideEffectContext); } notifyListeners(); } catch (e) { if (retries == 0) { status = MutationStatus.error; error = e; - for (final onError in onErrorListeners) { - onError(error); + for (final onError in _onErrorListeners) { + onError(error, variables, _sideEffectContext); } notifyListeners(); } else { @@ -97,13 +110,13 @@ class Mutation extends BaseOperation { while (retryAttempts <= retries) { await Future.delayed(retryDelay); try { - for (final onMutate in onMutateListeners) { - onMutate(variables); + for (final onMutate in _onMutateListeners) { + _sideEffectContext = onMutate(variables); } data = await task(mutationKey, variables); status = MutationStatus.success; - for (final onData in onDataListeners) { - onData(data!); + for (final onData in _onDataListeners) { + onData(data!, variables, _sideEffectContext); } notifyListeners(); break; @@ -111,8 +124,8 @@ class Mutation extends BaseOperation { if (retryAttempts == retries) { status = MutationStatus.error; error = e; - for (final onError in onErrorListeners) { - onError(error); + for (final onError in _onErrorListeners) { + onError(error, variables, _sideEffectContext); } notifyListeners(); } @@ -123,20 +136,46 @@ class Mutation extends BaseOperation { } } + void addDataListener(MutationListener listener) { + _onDataListeners.add(listener); + } + + void addErrorListener(MutationListener listener) { + _onErrorListeners.add(listener); + } + + void addMutateListener(MutationListenerReturnable listener) { + _onMutateListeners.add(listener); + } + + void removeDataListener(MutationListener listener) { + _onDataListeners.remove(listener); + } + + void removeErrorListener(MutationListener listener) { + _onErrorListeners.remove(listener); + } + + void removeMutateListener(MutationListenerReturnable listener) { + _onMutateListeners.remove(listener); + } + void mutate( V variables, { - MutationListener? onData, - MutationListener? onError, + MutationListener? onData, + MutationListener? onError, }) { - if (onData != null) onDataListeners.add(onData); - if (onError != null) onErrorListeners.add(onError); + _variables = variables; + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); _execute(variables).then((_) { - onDataListeners.remove(onData); - onErrorListeners.remove(onError); + _onDataListeners.remove(onData); + _onErrorListeners.remove(onError); }); } Future mutateAsync(V variables) async { + _variables = variables; return await _execute(variables).then((_) => data); } @@ -155,10 +194,11 @@ class Mutation extends BaseOperation { data = null; retryAttempts = 0; updatedAt = DateTime.now(); - onDataListeners.clear(); - onErrorListeners.clear(); + _onDataListeners.clear(); + _onErrorListeners.clear(); status = MutationStatus.idle; - onMutateListeners.clear(); + _onMutateListeners.clear(); + _sideEffectContext = null; } A? cast() => this is A ? this as A : null; diff --git a/packages/fl_query/lib/src/mutation_builder.dart b/packages/fl_query/lib/src/mutation_builder.dart index e3d8304..c181c79 100644 --- a/packages/fl_query/lib/src/mutation_builder.dart +++ b/packages/fl_query/lib/src/mutation_builder.dart @@ -10,15 +10,15 @@ class MutationBuilder extends StatefulWidget { /// Called when the query returns new data, on query /// refetch or query gets expired - final MutationListener? onData; + final MutationListener? onData; /// Called when the query returns error - final MutationListener? onError; + final MutationListener? onError; /// called right before the mutation is about to run /// /// perfect scenario for doing optimistic updates - final MutationListener? onMutate; + final MutationListenerReturnable? onMutate; const MutationBuilder({ required this.job, @@ -66,19 +66,17 @@ class _MutationBuilderState init(); } else { if (oldWidget.onData != widget.onData && oldWidget.onData != null) { - mutation?.onDataListeners.remove(oldWidget.onData); - if (widget.onData != null) - mutation?.onDataListeners.add(widget.onData!); + mutation?.removeDataListener(oldWidget.onData!); + if (widget.onData != null) mutation?.addDataListener(widget.onData!); } if (oldWidget.onError != widget.onError && oldWidget.onError != null) { - mutation?.onErrorListeners.remove(oldWidget.onError); - if (widget.onError != null) - mutation?.onErrorListeners.add(widget.onError!); + mutation?.removeErrorListener(oldWidget.onError!); + if (widget.onError != null) mutation?.addErrorListener(widget.onError!); } if (oldWidget.onMutate != widget.onMutate && oldWidget.onMutate != null) { - mutation?.onMutateListeners.remove(oldWidget.onMutate); + mutation?.removeMutateListener(oldWidget.onMutate!); if (widget.onMutate != null) - mutation?.onMutateListeners.add(widget.onMutate!); + mutation?.addMutateListener(widget.onMutate!); } } super.didUpdateWidget(oldWidget); @@ -92,11 +90,9 @@ class _MutationBuilderState void _mutationDispose() { mutation?.unmount(uKey); - if (widget.onData != null) mutation?.onDataListeners.remove(widget.onData); - if (widget.onError != null) - mutation?.onErrorListeners.remove(widget.onError); - if (widget.onMutate != null) - mutation?.onMutateListeners.remove(widget.onMutate); + if (widget.onData != null) mutation?.addDataListener(widget.onData!); + if (widget.onError != null) mutation?.addErrorListener(widget.onError!); + if (widget.onMutate != null) mutation?.addMutateListener(widget.onMutate!); } @override diff --git a/packages/fl_query/lib/src/query.dart b/packages/fl_query/lib/src/query.dart index 655b9cf..cce63c4 100644 --- a/packages/fl_query/lib/src/query.dart +++ b/packages/fl_query/lib/src/query.dart @@ -56,10 +56,8 @@ class Query extends BaseOperation { QueryStatus status; - @protected - final Set> onDataListeners = Set>(); - @protected - final Set> onErrorListeners = + final Set> _onDataListeners = Set>(); + final Set> _onErrorListeners = Set>(); // externalData will always be passed to the task Callback @@ -72,6 +70,8 @@ class Query extends BaseOperation { Timer? _refetchIntervalTimer; + Connectivity _connectivity; + Query({ required this.queryKey, required this.task, @@ -85,6 +85,7 @@ class Query extends BaseOperation { this.refetchOnReconnect, this.refetchInterval, this.enabled = true, + Connectivity? connectivity, T? initialData, QueryListener? onData, QueryListener? onError, @@ -92,9 +93,10 @@ class Query extends BaseOperation { _initialData = initialData, _externalData = externalData, status = QueryStatus.idle, + _connectivity = connectivity ?? Connectivity(), super(data: initialData) { - if (onData != null) onDataListeners.add(onData); - if (onError != null) onErrorListeners.add(onError); + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); if (refetchInterval != null && refetchInterval != Duration.zero) { _refetchIntervalTimer = _createRefetchTimer(); @@ -117,14 +119,18 @@ class Query extends BaseOperation { refetchOnMount = options.refetchOnMount, refetchOnReconnect = options.refetchOnReconnect, status = QueryStatus.idle, + _connectivity = options.connectivity ?? Connectivity(), super( cacheTime: options.cacheTime ?? const Duration(minutes: 5), retries: options.retries ?? 3, retryDelay: options.retryDelay ?? const Duration(milliseconds: 200), data: options.initialData, ) { - if (onData != null) onDataListeners.add(onData); - if (onError != null) onErrorListeners.add(onError); + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); + if (refetchInterval != null && refetchInterval != Duration.zero) { + _refetchIntervalTimer = _createRefetchTimer(); + } } // all getters & setters @@ -138,9 +144,7 @@ class Query extends BaseOperation { (_) async { // only refetch if its connected to the internet or refetch will // always result in error while there's no internet - if (isStale && - isConnectedToInternet(await Connectivity().checkConnectivity())) - await refetch(); + if (isStale && await isInternetConnected()) await refetch(); }, ); } @@ -157,7 +161,7 @@ class Query extends BaseOperation { _prevUsedExternalData = _externalData; updatedAt = DateTime.now(); status = QueryStatus.success; - for (final onData in onDataListeners) { + for (final onData in _onDataListeners) { onData(data!); } notifyListeners(); @@ -165,7 +169,7 @@ class Query extends BaseOperation { if (retries == 0) { status = QueryStatus.error; error = e; - for (final onError in onErrorListeners) { + for (final onError in _onErrorListeners) { onError(error); } notifyListeners(); @@ -180,7 +184,7 @@ class Query extends BaseOperation { ); _prevUsedExternalData = _externalData; status = QueryStatus.success; - for (final onData in onDataListeners) { + for (final onData in _onDataListeners) { await onData(data!); } notifyListeners(); @@ -189,7 +193,7 @@ class Query extends BaseOperation { if (retryAttempts == retries) { status = QueryStatus.error; error = e; - for (final onError in onErrorListeners) { + for (final onError in _onErrorListeners) { await onError(error); } notifyListeners(); @@ -202,6 +206,22 @@ class Query extends BaseOperation { } } + void addDataListener(QueryListener listener) { + _onDataListeners.add(listener); + } + + void addErrorListener(QueryListener listener) { + _onErrorListeners.add(listener); + } + + void removeDataListener(QueryListener listener) { + _onDataListeners.remove(listener); + } + + void removeErrorListener(QueryListener listener) { + _onErrorListeners.remove(listener); + } + Future fetch() async { if (!enabled) return null; @@ -258,8 +278,8 @@ class Query extends BaseOperation { fetched = false; status = QueryStatus.idle; retryAttempts = 0; - onDataListeners.clear(); - onErrorListeners.clear(); + _onDataListeners.clear(); + _onErrorListeners.clear(); mounts.clear(); } @@ -290,13 +310,27 @@ class Query extends BaseOperation { notifyListeners(); } + Future isInternetConnected() async { + return isConnectedToInternet(await _connectivity.checkConnectivity()); + } + + /// invalidates the query + void invalidate() { + /// subtracting [staleTime] from [updatedAt] as staleTime=Duration.zero + /// indicates the query must never become stale but subtracting the + /// [staleTime] will always revert the updatedAt time to the default + /// time whenever isStale is called + updatedAt = updatedAt.subtract(_staleTime); + notifyListeners(); + } + bool get isStale { /// when [_staleTime] is [Duration.zero], the query will always be /// stale & will never refetch in the background. But can be inactive /// if [mounts.length] become zero if (_staleTime == Duration.zero) return false; - // when current DateTime is after [update_at + stale_time] it means + // when [DateTime.now()] is after [update_at + stale_time] it means // the data has become stale return DateTime.now().isAfter(updatedAt.add(_staleTime)); } @@ -315,10 +349,8 @@ class Query extends BaseOperation { void mount(ValueKey uKey) { super.mount(uKey); if (refetchOnMount == true && isStale) { - Connectivity().checkConnectivity().then((status) async { - if (isConnectedToInternet(status)) { - await refetch(); - } + this.isInternetConnected().then((isConnected) async { + if (isConnected) await refetch(); }); } } diff --git a/packages/fl_query/lib/src/query_bowl.dart b/packages/fl_query/lib/src/query_bowl.dart index fb11037..887a58c 100644 --- a/packages/fl_query/lib/src/query_bowl.dart +++ b/packages/fl_query/lib/src/query_bowl.dart @@ -282,6 +282,39 @@ class QueryBowl extends InheritedWidget { _addMutation = addMutation, super(child: child, key: key); + Query _createQueryWithDefaults( + QueryJob options, + Outside externalData, + ) { + final query = Query.fromOptions( + options, + externalData: externalData, + queryBowl: this, + ); + query.updateDefaultOptions( + cacheTime: cacheTime, + staleTime: staleTime, + refetchInterval: refetchInterval, + refetchOnMount: refetchOnMount, + refetchOnReconnect: refetchOnReconnect, + ); + return query; + } + + Future prefetchQuery( + QueryJob options, { + required Outside externalData, + }) async { + final prevQuery = + _queries.firstWhereOrNull((q) => q.queryKey == options.queryKey); + if (prevQuery != null && prevQuery is Query) + return prevQuery.data; + + final query = _createQueryWithDefaults(options, externalData); + _addQuery(query); + return await query.fetch(); + } + /// !⚠️**Warning** only for internal library usage @protected Future fetchQuery( @@ -303,8 +336,8 @@ class QueryBowl extends InheritedWidget { externalData, ); prevQuery.mount(key); - if (onData != null) prevQuery.onDataListeners.add(onData); - if (onError != null) prevQuery.onErrorListeners.add(onError); + if (onData != null) prevQuery.addDataListener(onData); + if (onError != null) prevQuery.addErrorListener(onError); if (!prevQuery.hasData || hasExternalDataChanged) { if (hasExternalDataChanged) prevQuery.setExternalData(externalData); @@ -314,19 +347,7 @@ class QueryBowl extends InheritedWidget { return prevQuery.data; } - /// populating with default configurations - options.refetchInterval ??= refetchInterval; - options.staleTime ??= staleTime; - options.cacheTime ??= cacheTime; - options.refetchOnMount ??= refetchOnMount; - options.refetchOnReconnect ??= refetchOnReconnect; - final query = Query.fromOptions( - options, - externalData: externalData, - queryBowl: this, - onData: onData, - onError: onError, - ); + final query = _createQueryWithDefaults(options, externalData); query.mount(key); _addQuery(query); return await query.fetch(); @@ -355,25 +376,14 @@ class QueryBowl extends InheritedWidget { prevQuery.setExternalData(externalData); } prevQuery.mount(key); - if (onData != null) prevQuery.onDataListeners.add(onData); - if (onError != null) prevQuery.onErrorListeners.add(onError); + if (onData != null) prevQuery.addDataListener(onData); + if (onError != null) prevQuery.addErrorListener(onError); // mounting the widget that is using the query in the prevQuery return prevQuery; } - final query = Query.fromOptions( - queryJob, - externalData: externalData, - queryBowl: this, - ); - if (onData != null) query.onDataListeners.add(onData); - if (onError != null) query.onErrorListeners.add(onError); - query.updateDefaultOptions( - cacheTime: cacheTime, - staleTime: staleTime, - refetchInterval: refetchInterval, - refetchOnMount: refetchOnMount, - refetchOnReconnect: refetchOnReconnect, - ); + final query = _createQueryWithDefaults(queryJob, externalData); + if (onData != null) query.addDataListener(onData); + if (onError != null) query.addErrorListener(onError); query.mount(key); _addQuery(query); return query; @@ -383,17 +393,17 @@ class QueryBowl extends InheritedWidget { @protected Mutation addMutation( MutationJob mutationJob, { - final MutationListener? onData, - final MutationListener? onError, - final MutationListener? onMutate, + final MutationListener? onData, + final MutationListener? onError, + final MutationListenerReturnable? onMutate, required ValueKey key, }) { final prevMutation = _mutations.firstWhereOrNull( (prevMutation) => prevMutation.mutationKey == mutationJob.mutationKey); if (prevMutation != null && prevMutation is Mutation) { - if (onData != null) prevMutation.onDataListeners.add(onData); - if (onError != null) prevMutation.onErrorListeners.add(onError); - if (onMutate != null) prevMutation.onMutateListeners.add(onMutate); + if (onData != null) prevMutation.addDataListener(onData); + if (onError != null) prevMutation.addErrorListener(onError); + if (onMutate != null) prevMutation.addMutateListener(onMutate); prevMutation.mount(key); return prevMutation; } else { @@ -401,9 +411,9 @@ class QueryBowl extends InheritedWidget { mutationJob, queryBowl: this, ); - if (onData != null) mutation.onDataListeners.add(onData); - if (onError != null) mutation.onErrorListeners.add(onError); - if (onMutate != null) mutation.onMutateListeners.add(onMutate); + if (onData != null) mutation.addDataListener(onData); + if (onError != null) mutation.addErrorListener(onError); + if (onMutate != null) mutation.addMutateListener(onMutate); mutation.updateDefaultOptions(cacheTime: cacheTime); mutation.mount(key); _addMutation(mutation); @@ -461,27 +471,35 @@ class QueryBowl extends InheritedWidget { } /// resets all the queries matching the passed List of queryKeys + /// + /// If an empty list of [queryKeys] is passed then all of the queries + /// will be reset void resetQueries(List queryKeys) { for (final query in _queries) { - if (!queryKeys.contains(query.queryKey)) continue; + if (queryKeys.isNotEmpty && !queryKeys.contains(query.queryKey)) continue; query.reset(); } } /// makes all the queries matching the passed List of queryKeys stale + /// + /// If an empty list of [queryKeys] is passed then all of the queries + /// will be invalidated void invalidateQueries(List queryKeys) { for (final query in _queries) { - if (!queryKeys.contains(query.queryKey)) continue; - // TODO: Implement Invaldiate Queries + if (queryKeys.isNotEmpty && queryKeys.contains(query.queryKey)) continue; + query.invalidate(); } } /// refetches all the queries matching the passed List of queryKeys + /// + /// If an empty list of [queryKeys] is passed then all of the queries + /// will be refetched Future refetchQueries(List queryKeys) async { for (final query in _queries) { - if (queryKeys.contains(query.queryKey)) { - await query.refetch(); - } + if (queryKeys.isNotEmpty && queryKeys.contains(query.queryKey)) continue; + await query.refetch(); } } diff --git a/packages/fl_query/lib/src/query_builder.dart b/packages/fl_query/lib/src/query_builder.dart index 5528560..a764976 100644 --- a/packages/fl_query/lib/src/query_builder.dart +++ b/packages/fl_query/lib/src/query_builder.dart @@ -90,17 +90,16 @@ class _QueryBuilderState .getQuery(widget.job.queryKey) ?.setExternalData(widget.externalData); } - if (hasOnDataChanged) query?.onDataListeners.remove(oldWidget.onData); - if (hasOnErrorChanged) query?.onErrorListeners.remove(oldWidget.onError); + if (hasOnDataChanged) query?.removeDataListener(oldWidget.onData!); + if (hasOnErrorChanged) query?.removeErrorListener(oldWidget.onError!); } else { if (hasOnDataChanged) { - query?.onDataListeners.remove(oldWidget.onData); - if (widget.onData != null) query?.onDataListeners.add(widget.onData!); + query?.removeDataListener(oldWidget.onData!); + if (widget.onData != null) query?.addDataListener(widget.onData!); } if (hasOnErrorChanged) { - query?.onErrorListeners.remove(oldWidget.onError); - if (widget.onError != null) - query?.onErrorListeners.add(widget.onError!); + query?.removeErrorListener(oldWidget.onError!); + if (widget.onError != null) query?.addErrorListener(widget.onError!); } } super.didUpdateWidget(oldWidget); @@ -108,8 +107,8 @@ class _QueryBuilderState _queryDispose() { query?.unmount(uKey); - if (widget.onData != null) query?.onDataListeners.remove(widget.onData); - if (widget.onError != null) query?.onErrorListeners.remove(widget.onError); + if (widget.onData != null) query?.removeDataListener(widget.onData!); + if (widget.onError != null) query?.removeErrorListener(widget.onError!); } @override diff --git a/packages/fl_query/test/query_test.dart b/packages/fl_query/test/query_test.dart index 5184bf5..5823426 100644 --- a/packages/fl_query/test/query_test.dart +++ b/packages/fl_query/test/query_test.dart @@ -1,15 +1,18 @@ import 'dart:math'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fl_query/src/models/query_job.dart'; import 'package:fl_query/src/query.dart'; import 'package:fl_query/src/query_bowl.dart'; +import 'package:fl_query/src/utils.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'query_test.mocks.dart'; @GenerateMocks( - [QueryBowl], + [QueryBowl, Connectivity], customMocks: [ MockSpec>( as: #MockQueryJobVoidObject, @@ -129,7 +132,7 @@ void main() { "onData listeners are called When new data is fetched and set", () async { int count = 0; - query.onDataListeners.add((_) { + query.addDataListener((_) { count++; }); await query.fetch(); @@ -138,20 +141,111 @@ void main() { ); test( "onError listeners are called When any error occurs", - () {}, + () async { + reset(queryJob); + when(queryJob.queryKey).thenReturn("test"); + when(queryJob.task).thenAnswer( + (_) => (_, __) => Future.error("Error"), + ); + query = Query.fromOptions( + queryJob, + externalData: null, + queryBowl: queryBowl, + ); + int count = 0; + query.addErrorListener((_) { + count++; + }); + await query.fetch(); + expect(count, 1); + }, ); - test("query should become stale after defined amount time", () {}); + test("query should become stale after defined amount time", () async { + when(queryJob.staleTime).thenReturn(Duration(milliseconds: 500)); + query = Query.fromOptions( + queryJob, + externalData: null, + queryBowl: queryBowl, + ); + expect(query.isStale, isFalse); + await Future.delayed(Duration(milliseconds: 300)); + expect(query.isStale, isFalse); + await Future.delayed(Duration(milliseconds: 600)); + expect(query.isStale, isTrue); + }); test( "query should refetch in interval When refetchInterval is specified", - () {}, + () async { + reset(queryJob); + when(queryJob.queryKey).thenReturn("test"); + when(queryJob.task).thenAnswer( + (_) => (_, __) => Future.value(Random().nextInt(100)), + ); + when(queryJob.staleTime).thenReturn( + Duration(milliseconds: 1), + ); + when(queryJob.refetchInterval).thenReturn( + Duration(milliseconds: 200), + ); + query = Query.fromOptions( + queryJob, + externalData: null, + queryBowl: queryBowl, + ); + + final data = await query.fetch(); + await Future.delayed(Duration(milliseconds: 400)); + expect(data, isNot(equals(await query.fetch()))); + }, ); test( "query should revalidate When a new caller gets mounted", - () {}, + () async { + reset(queryJob); + when(queryJob.queryKey).thenReturn("test"); + when(queryJob.task).thenAnswer( + (_) => (_, __) => Future.value(Random().nextInt(150)), + ); + when(queryJob.refetchOnMount).thenReturn(true); + query = Query.fromOptions( + queryJob, + externalData: null, + queryBowl: queryBowl, + ); + final data = await query.fetch(); + query.invalidate(); + query.mount(ValueKey(uuid.v4())); + await Future.delayed(Duration(milliseconds: 100)); + expect(data, isNot(equals(await query.fetch()))); + expect(query.refetchCount, 1); + }, ); test( "query should not revalidate When there's no Internet Connectivity and a new caller gets mounted", - () {}, + () async { + final connectivityMock = MockConnectivity(); + when(connectivityMock.checkConnectivity()).thenAnswer( + (_) async => ConnectivityResult.none, + ); + reset(queryJob); + when(queryJob.queryKey).thenReturn("test"); + when(queryJob.task).thenAnswer( + (_) => (_, __) => Future.value(Random().nextInt(150)), + ); + when(queryJob.connectivity).thenReturn(connectivityMock); + when(queryJob.refetchOnMount).thenReturn(true); + query = Query.fromOptions( + queryJob, + externalData: null, + queryBowl: queryBowl, + ); + final data = await query.fetch(); + query.invalidate(); + query.mount(ValueKey(uuid.v4())); + await Future.delayed(Duration(milliseconds: 100)); + expect(data, equals(await query.fetch())); + expect(query.refetchCount, 0); + }, ); }); } diff --git a/packages/fl_query/test/query_test.mocks.dart b/packages/fl_query/test/query_test.mocks.dart index b5a38b5..33fdfbb 100644 --- a/packages/fl_query/test/query_test.mocks.dart +++ b/packages/fl_query/test/query_test.mocks.dart @@ -4,13 +4,13 @@ import 'dart:async' as _i7; +import 'package:connectivity_plus/connectivity_plus.dart' as _i10; import 'package:fl_query/src/models/mutation_job.dart' as _i9; import 'package:fl_query/src/models/query_job.dart' as _i8; import 'package:fl_query/src/mutation.dart' as _i4; import 'package:fl_query/src/query.dart' as _i3; import 'package:fl_query/src/query_bowl.dart' as _i6; import 'package:flutter/foundation.dart' as _i5; -import 'package:flutter/rendering.dart' as _i10; import 'package:flutter/widgets.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; @@ -136,9 +136,9 @@ class MockQueryBowl extends _i1.Mock implements _i6.QueryBowl { @override _i4.Mutation addMutation( _i9.MutationJob? mutationJob, - {_i4.MutationListener? onData, - _i4.MutationListener? onError, - _i4.MutationListener? onMutate, + {_i4.MutationListener? onData, + _i4.MutationListener? onError, + _i4.MutationListenerReturnable? onMutate, _i2.ValueKey? key}) => (super.noSuchMethod( Invocation.method(#addMutation, [ @@ -190,7 +190,7 @@ class MockQueryBowl extends _i1.Mock implements _i6.QueryBowl { .noSuchMethod(Invocation.method(#toStringShort, []), returnValue: '') as String); @override - void debugFillProperties(_i10.DiagnosticPropertiesBuilder? properties) => + void debugFillProperties(_i5.DiagnosticPropertiesBuilder? properties) => super.noSuchMethod(Invocation.method(#debugFillProperties, [properties]), returnValueForMissingStub: null); @override @@ -229,6 +229,27 @@ class MockQueryBowl extends _i1.Mock implements _i6.QueryBowl { super.toString(); } +/// A class which mocks [Connectivity]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConnectivity extends _i1.Mock implements _i10.Connectivity { + MockConnectivity() { + _i1.throwOnMissingStub(this); + } + + @override + _i7.Stream<_i10.ConnectivityResult> get onConnectivityChanged => + (super.noSuchMethod(Invocation.getter(#onConnectivityChanged), + returnValue: Stream<_i10.ConnectivityResult>.empty()) + as _i7.Stream<_i10.ConnectivityResult>); + @override + _i7.Future<_i10.ConnectivityResult> checkConnectivity() => + (super.noSuchMethod(Invocation.method(#checkConnectivity, []), + returnValue: Future<_i10.ConnectivityResult>.value( + _i10.ConnectivityResult.bluetooth)) + as _i7.Future<_i10.ConnectivityResult>); +} + /// A class which mocks [QueryJob]. /// /// See the documentation for Mockito's code generation for more information. @@ -271,6 +292,10 @@ class MockQueryJobVoidObject extends _i1.Mock super.noSuchMethod(Invocation.setter(#refetchInterval, _refetchInterval), returnValueForMissingStub: null); @override + set connectivity(_i10.Connectivity? _connectivity) => + super.noSuchMethod(Invocation.setter(#connectivity, _connectivity), + returnValueForMissingStub: null); + @override String get queryKey => (super.noSuchMethod(Invocation.getter(#queryKey), returnValue: '') as String); diff --git a/packages/fl_query_hooks/.flutter-plugins-dependencies b/packages/fl_query_hooks/.flutter-plugins-dependencies index f0993c2..3722cc1 100644 --- a/packages/fl_query_hooks/.flutter-plugins-dependencies +++ b/packages/fl_query_hooks/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.3/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]}],"date_created":"2022-07-19 23:08:04.911997","version":"3.0.1"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/home/krtirtho/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.3/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]}],"date_created":"2022-07-28 10:55:26.979398","version":"3.0.1"} \ No newline at end of file diff --git a/packages/fl_query_hooks/lib/src/use_mutation.dart b/packages/fl_query_hooks/lib/src/use_mutation.dart index 7267e1d..c3c80d2 100644 --- a/packages/fl_query_hooks/lib/src/use_mutation.dart +++ b/packages/fl_query_hooks/lib/src/use_mutation.dart @@ -8,15 +8,15 @@ Mutation useMutation({ /// Called when the query returns new data, on query /// refetch or query gets expired - MutationListener? onData, + MutationListener? onData, /// Called when the query returns error - MutationListener? onError, + MutationListener? onError, /// called right before the mutation is about to run /// /// perfect scenario for doing optimistic updates - MutationListener? onMutate, + MutationListenerReturnable? onMutate, List? keys, }) { final context = useContext(); @@ -37,9 +37,9 @@ Mutation useMutation({ final disposeMutation = useCallback(() { mutation.value.unmount(uKey); - if (onData != null) mutation.value.onDataListeners.remove(onData); - if (onError != null) mutation.value.onErrorListeners.remove(onError); - if (onMutate != null) mutation.value.onMutateListeners.remove(onMutate); + if (onData != null) mutation.value.removeDataListener(onData); + if (onError != null) mutation.value.removeErrorListener(onError); + if (onMutate != null) mutation.value.removeMutateListener(onMutate); }, [mutation.value, onData, onError, onMutate]); final oldJob = usePrevious(job); @@ -58,16 +58,16 @@ Mutation useMutation({ init(); } else { if (oldOnData != onData && oldOnData != null) { - mutation.value.onDataListeners.remove(oldOnData); - if (onData != null) mutation.value.onDataListeners.add(onData); + mutation.value.removeDataListener(oldOnData); + if (onData != null) mutation.value.addDataListener(onData); } if (oldOnError != onError && oldOnError != null) { - mutation.value.onErrorListeners.remove(oldOnError); - if (onError != null) mutation.value.onErrorListeners.add(onError); + mutation.value.removeErrorListener(oldOnError); + if (onError != null) mutation.value.addErrorListener(onError); } if (oldOnMutate != onMutate && oldOnMutate != null) { - mutation.value.onMutateListeners.remove(oldOnMutate); - if (onMutate != null) mutation.value.onMutateListeners.add(onMutate); + mutation.value.removeMutateListener(oldOnMutate); + if (onMutate != null) mutation.value.addMutateListener(onMutate); } } return null; diff --git a/packages/fl_query_hooks/lib/src/use_query.dart b/packages/fl_query_hooks/lib/src/use_query.dart index 9ff102e..7bf1525 100644 --- a/packages/fl_query_hooks/lib/src/use_query.dart +++ b/packages/fl_query_hooks/lib/src/use_query.dart @@ -52,8 +52,8 @@ Query useQuery({ final disposeQuery = useCallback(() { query.value.unmount(uKey); - if (onData != null) query.value.onDataListeners.remove(onData); - if (onError != null) query.value.onErrorListeners.remove(onError); + if (onData != null) query.value.removeDataListener(onData); + if (onError != null) query.value.removeErrorListener(onError); }, [query.value, onData, onError, uKey]); useEffect(() { @@ -85,16 +85,16 @@ Query useQuery({ ?.setExternalData(externalData); } - if (hasOnDataChanged) query.value.onDataListeners.remove(oldOnData); - if (hasOnErrorChanged) query.value.onErrorListeners.remove(oldOnError); + if (hasOnDataChanged) query.value.removeDataListener(oldOnData); + if (hasOnErrorChanged) query.value.removeErrorListener(oldOnError); } else { if (hasOnDataChanged) { - query.value.onDataListeners.remove(oldOnData); - if (onData != null) query.value.onDataListeners.add(onData); + query.value.removeDataListener(oldOnData); + if (onData != null) query.value.addDataListener(onData); } if (hasOnErrorChanged) { - query.value.onErrorListeners.remove(oldOnError); - if (onError != null) query.value.onErrorListeners.add(onError); + query.value.removeErrorListener(oldOnError); + if (onError != null) query.value.addErrorListener(onError); } } return null;