From e0c70d153f62a9bcf1c6f59addad74636dbd3b6d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Jun 2022 17:16:06 +0600 Subject: [PATCH] Internal API change now query & mutation share same base class queries are now self-refetchable queryJobs & mutationJobs now support dynamic key allowing dynamic Query/Mutation creation query status & mutation status are now just on point & accurate --- packages/example/lib/main.dart | 24 ++- packages/example/lib/mutation_example.dart | 3 +- .../example/lib/query_with_external_data.dart | 3 +- packages/fl_query/lib/base_operation.dart | 65 ++++++ packages/fl_query/lib/fl_query.dart | 3 + .../fl_query/lib/models/mutation_job.dart | 32 ++- packages/fl_query/lib/models/query_job.dart | 51 ++++- packages/fl_query/lib/mutation.dart | 113 +++++------ packages/fl_query/lib/query.dart | 185 ++++++++++-------- packages/fl_query/lib/query_bowl.dart | 77 ++++++-- packages/fl_query/lib/query_builder.dart | 11 +- packages/fl_query/lib/utils.dart | 30 +++ packages/fl_query/test/query_test.dart | 4 +- 13 files changed, 417 insertions(+), 184 deletions(-) create mode 100644 packages/fl_query/lib/base_operation.dart diff --git a/packages/example/lib/main.dart b/packages/example/lib/main.dart index a88985f..f5371fb 100644 --- a/packages/example/lib/main.dart +++ b/packages/example/lib/main.dart @@ -53,7 +53,25 @@ class MyHomePage extends StatefulWidget { State createState() => _MyHomePageState(); } -class _MyHomePageState extends State { +class _MyHomePageState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + print("LIFE CYCLE STATE: $state"); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -68,7 +86,7 @@ class _MyHomePageState extends State { job: successJob, externalData: null, builder: (context, query) { - if (query.isLoading || query.isRefetching) { + if (!query.hasData || query.isLoading || query.isRefetching) { return const CircularProgressIndicator(); } return TextButton( @@ -83,7 +101,7 @@ class _MyHomePageState extends State { job: successJob, externalData: null, builder: (context, query) { - if (query.isLoading || query.isRefetching) { + if (!query.hasData || query.isLoading || query.isRefetching) { return const CircularProgressIndicator(); } return ElevatedButton( diff --git a/packages/example/lib/mutation_example.dart b/packages/example/lib/mutation_example.dart index b582a15..5147425 100644 --- a/packages/example/lib/mutation_example.dart +++ b/packages/example/lib/mutation_example.dart @@ -1,8 +1,7 @@ import 'dart:convert'; import 'dart:math'; -import 'package:fl_query/models/mutation_job.dart'; -import 'package:fl_query/mutation_builder.dart'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; diff --git a/packages/example/lib/query_with_external_data.dart b/packages/example/lib/query_with_external_data.dart index 600893b..d500c7c 100644 --- a/packages/example/lib/query_with_external_data.dart +++ b/packages/example/lib/query_with_external_data.dart @@ -22,7 +22,7 @@ class QueryWithExternalData extends StatelessWidget { job: jobWithExternalData, externalData: (Random().nextDouble() * 200).toString(), builder: (context, query) { - if (query.isLoading || query.isLoading || query.data == null) { + if (query.isLoading || query.isRefetching || !query.hasData) { return const CircularProgressIndicator(); } return Container( @@ -32,6 +32,7 @@ class QueryWithExternalData extends StatelessWidget { shape: BoxShape.circle, color: Colors.blue, ), + child: Text(query.externalData), ); }, ), diff --git a/packages/fl_query/lib/base_operation.dart b/packages/fl_query/lib/base_operation.dart new file mode 100644 index 0000000..89ead8a --- /dev/null +++ b/packages/fl_query/lib/base_operation.dart @@ -0,0 +1,65 @@ +import 'package:flutter/widgets.dart'; + +abstract class BaseOperation extends ChangeNotifier { + /// The number of times the query should refetch in the time of error + /// before giving up + final int retries; + final Duration retryDelay; + + // got from global options + @protected + Duration cacheTime; + + // all properties + Data? data; + dynamic error; + StatusType status; + + /// total count of how many times the query retried to get a successful + /// result + int retryAttempts = 0; + DateTime updatedAt; + + @protected + bool fetched = false; + + /// used for keeping track of query activity. If the are no mounts & + /// the passed cached time is over than the query is removed from + /// storage/cache + Set> _mounts = {}; + + BaseOperation({ + required this.cacheTime, + required this.retries, + required this.retryDelay, + required this.status, + this.data, + }) : updatedAt = DateTime.now(); + + void mount(ValueKey uKey) { + _mounts.add(uKey); + } + + void unmount(ValueKey uKey) { + if (_mounts.length == 1) { + Future.delayed(cacheTime, () { + _mounts.remove(uKey); + // for letting know QueryBowl that this one's time has come for + // getting crushed + notifyListeners(); + }); + } else { + _mounts.remove(uKey); + } + } + + Set> get mounts => _mounts; + + bool get isSuccess; + bool get isError; + bool get isLoading; + bool get isIdle; + bool get isInactive => mounts.isEmpty; + bool get hasData => isSuccess && data != null; + bool get hasError => isError && error != null; +} diff --git a/packages/fl_query/lib/fl_query.dart b/packages/fl_query/lib/fl_query.dart index 20231e8..676f93d 100644 --- a/packages/fl_query/lib/fl_query.dart +++ b/packages/fl_query/lib/fl_query.dart @@ -3,4 +3,7 @@ library fl_query; export 'query.dart'; export 'query_bowl.dart'; export 'query_builder.dart'; +export 'mutation.dart'; +export 'mutation_builder.dart'; export 'models/query_job.dart'; +export 'models/mutation_job.dart'; diff --git a/packages/fl_query/lib/models/mutation_job.dart b/packages/fl_query/lib/models/mutation_job.dart index 9487da2..4483c5c 100644 --- a/packages/fl_query/lib/models/mutation_job.dart +++ b/packages/fl_query/lib/models/mutation_job.dart @@ -1,17 +1,43 @@ import 'package:fl_query/mutation.dart'; class MutationJob { - final String mutationKey; + String _mutationKey; MutationTaskFunction task; final int? retries; final Duration? retryDelay; final Duration? cacheTime; MutationJob({ - required this.mutationKey, + required String mutationKey, required this.task, this.retries, this.retryDelay, this.cacheTime, - }); + }) : _mutationKey = mutationKey; + + String get mutationKey => _mutationKey; + + static MutationJob Function(String queryKey) + withVariableKey({ + required MutationTaskFunction task, + + /// a extra key joined with mutationKey by a '#' + /// + /// useful for matching a group mutation + String? preMutationKey, + int? retries, + Duration? retryDelay, + Duration? cacheTime, + }) { + return (String mutationKey) { + if (preMutationKey != null) mutationKey = "$preMutationKey#$mutationKey"; + return MutationJob( + mutationKey: mutationKey, + task: task, + retries: retries, + retryDelay: retryDelay, + cacheTime: cacheTime, + ); + }; + } } diff --git a/packages/fl_query/lib/models/query_job.dart b/packages/fl_query/lib/models/query_job.dart index 8066789..23c8b4e 100644 --- a/packages/fl_query/lib/models/query_job.dart +++ b/packages/fl_query/lib/models/query_job.dart @@ -2,7 +2,7 @@ import 'package:fl_query/query.dart'; class QueryJob { // all params - final String queryKey; + String _queryKey; QueryTaskFunction task; final int? retries; final Duration? retryDelay; @@ -13,11 +13,14 @@ class QueryJob { final bool? enabled; // got from global options - final Duration? staleTime; - final Duration? cacheTime; + bool? refetchOnMount; + Duration? staleTime; + Duration? cacheTime; + + Duration? refetchInterval; QueryJob({ - required this.queryKey, + required String queryKey, required this.task, this.retries, this.retryDelay, @@ -25,5 +28,43 @@ class QueryJob { this.staleTime, this.cacheTime, this.enabled, - }); + this.refetchInterval, + this.refetchOnMount, + }) : _queryKey = queryKey; + + String get queryKey => _queryKey; + + static QueryJob Function(String queryKey) + withVariableKey({ + required QueryTaskFunction task, + + /// a extra key joined with queryKey by a '#' + /// + /// useful for matching a group query + String? preQueryKey, + int? retries, + Duration? retryDelay, + T? initialData, + Duration? staleTime, + Duration? cacheTime, + bool? enabled, + Duration? refetchInterval, + bool? refetchOnMount, + }) { + return (String queryKey) { + if (preQueryKey != null) queryKey = "$preQueryKey#$queryKey"; + return QueryJob( + queryKey: queryKey, + task: task, + retries: retries, + retryDelay: retryDelay, + initialData: initialData, + staleTime: staleTime, + cacheTime: cacheTime, + enabled: enabled, + refetchInterval: refetchInterval, + refetchOnMount: refetchOnMount, + ); + }; + } } diff --git a/packages/fl_query/lib/mutation.dart b/packages/fl_query/lib/mutation.dart index a08ec71..0fced36 100644 --- a/packages/fl_query/lib/mutation.dart +++ b/packages/fl_query/lib/mutation.dart @@ -1,40 +1,24 @@ import 'dart:async'; +import 'package:fl_query/base_operation.dart'; import 'package:fl_query/models/mutation_job.dart'; import 'package:flutter/widgets.dart'; enum MutationStatus { - failed, - succeed, - pending, + error, + success, + loading, + idle, } typedef MutationListener = FutureOr Function(T); typedef MutationTaskFunction = FutureOr Function(String, V); -class Mutation extends ChangeNotifier { +class Mutation extends BaseOperation { // all params final String mutationKey; MutationTaskFunction task; - final int retries; - final Duration retryDelay; - final Duration _cacheTime; - - // all properties - T? data; - dynamic error; - MutationStatus status; - - /// total count of how many times the query retried to get a successful - /// result - int retryAttempts = 0; - DateTime updatedAt; - - /// used for keeping track of mutation activity. If the are no mounts & - /// the passed cached time is over than the mutation is removed from - /// storage/cache - Set> _mounts = {}; @protected final Set> onDataListeners = {}; @@ -46,15 +30,13 @@ class Mutation extends ChangeNotifier { Mutation({ required this.mutationKey, required this.task, - required this.retries, - required this.retryDelay, + required super.retries, + required super.retryDelay, required Duration cacheTime, MutationListener? onData, MutationListener? onError, MutationListener? onMutate, - }) : status = MutationStatus.pending, - updatedAt = DateTime.now(), - _cacheTime = cacheTime { + }) : super(cacheTime: cacheTime, status: MutationStatus.idle) { if (onData != null) onDataListeners.add(onData); if (onError != null) onErrorListeners.add(onError); if (onMutate != null) onMutateListeners.add(onMutate); @@ -67,61 +49,38 @@ class Mutation extends ChangeNotifier { MutationListener? onMutate, }) : mutationKey = options.mutationKey, task = options.task, - retries = options.retries ?? 3, - retryDelay = options.retryDelay ?? const Duration(milliseconds: 200), - _cacheTime = options.cacheTime ?? const Duration(minutes: 5), - status = MutationStatus.pending, - updatedAt = DateTime.now() { + super( + retries: options.retries ?? 3, + retryDelay: options.retryDelay ?? const Duration(milliseconds: 200), + cacheTime: options.cacheTime ?? const Duration(minutes: 5), + status: MutationStatus.idle, + ) { if (onData != null) onDataListeners.add(onData); if (onError != null) onErrorListeners.add(onError); } - // all getters & setters - bool get hasData => data != null && error == null; - bool get hasError => - status == MutationStatus.failed && error != null && data == null; - bool get isLoading => - status == MutationStatus.pending && data == null && error == null; - bool get isSucceeded => status == MutationStatus.succeed && data != null; - bool get isIdle => isSucceeded && error == null; - bool get isInactive => _mounts.isEmpty; // all methods - void mount(ValueKey uKey) { - _mounts.add(uKey); - } - - void unmount(ValueKey uKey) { - if (_mounts.length == 1) { - Future.delayed(_cacheTime, () { - _mounts.remove(uKey); - // for letting know QueryBowl that this one's time has come for - // getting crushed - notifyListeners(); - }); - } else { - _mounts.remove(uKey); - } - } - /// Calls the task function & doesn't check if there's already /// cached data available - Future _execMutation(V variables) async { + Future _execute(V variables) async { try { + status = MutationStatus.loading; + notifyListeners(); retryAttempts = 0; for (final onMutate in onMutateListeners) { onMutate(variables); } data = await task(mutationKey, variables); updatedAt = DateTime.now(); - status = MutationStatus.succeed; + status = MutationStatus.success; for (final onData in onDataListeners) { onData(data!); } notifyListeners(); } catch (e) { if (retries == 0) { - status = MutationStatus.failed; + status = MutationStatus.error; error = e; for (final onError in onErrorListeners) { onError(error); @@ -136,7 +95,7 @@ class Mutation extends ChangeNotifier { onMutate(variables); } data = await task(mutationKey, variables); - status = MutationStatus.succeed; + status = MutationStatus.success; for (final onData in onDataListeners) { onData(data!); } @@ -144,7 +103,7 @@ class Mutation extends ChangeNotifier { break; } catch (e) { if (retryAttempts == retries) { - status = MutationStatus.failed; + status = MutationStatus.error; error = e; for (final onError in onErrorListeners) { onError(error); @@ -165,25 +124,45 @@ class Mutation extends ChangeNotifier { }) { if (onData != null) onDataListeners.add(onData); if (onError != null) onErrorListeners.add(onError); - _execMutation(variables).then((_) { + _execute(variables).then((_) { onDataListeners.remove(onData); onErrorListeners.remove(onError); }); } Future mutateAsync(V variables) async { - return await _execMutation(variables).then((_) => data); + return await _execute(variables).then((_) => data); } - reset() { + /// Update configurations of the mutation after already creating the + /// Mutation instance + void updateDefaultOptions({ + Duration? cacheTime, + }) { + if (this.cacheTime == Duration(minutes: 5) && cacheTime != null) + this.cacheTime = cacheTime; + + notifyListeners(); + } + + void reset() { data = null; retryAttempts = 0; updatedAt = DateTime.now(); onDataListeners.clear(); onErrorListeners.clear(); - status = MutationStatus.pending; + status = MutationStatus.idle; onMutateListeners.clear(); } A? cast() => this is A ? this as A : null; + + @override + bool get isError => status == MutationStatus.error; + @override + bool get isIdle => status == MutationStatus.idle; + @override + bool get isLoading => status == MutationStatus.loading; + @override + bool get isSuccess => status == MutationStatus.success; } diff --git a/packages/fl_query/lib/query.dart b/packages/fl_query/lib/query.dart index 3620750..23523c9 100644 --- a/packages/fl_query/lib/query.dart +++ b/packages/fl_query/lib/query.dart @@ -1,12 +1,25 @@ import 'dart:async'; +import 'package:fl_query/base_operation.dart'; import 'package:fl_query/models/query_job.dart'; import 'package:flutter/widgets.dart'; enum QueryStatus { - failed, - succeed, - pending, + /// in times when an error occurs + /// will get reset to idle on refetch/retry + error, + + /// when a query successfully executes + success, + + /// when the query is running (not refetching) + loading, + + /// when the query isn't yet fetched, re-fetched, or got reset + /// mostly when both [data] & [error] are null. Also [fetched] is false + idle, + + /// when the query is refetching (rerunning) refetching; } @@ -18,36 +31,23 @@ typedef ListenerUnsubscriber = void Function(); typedef QueryUpdateFunction = FutureOr Function(T? oldData); -class Query extends ChangeNotifier { +class Query extends BaseOperation { // all params final String queryKey; QueryTaskFunction task; - /// The number of times the query should refetch in the time of error - /// before giving up - final int retries; - final Duration retryDelay; + bool? refetchOnMount; + final T? _initialData; // got from global options - final Duration _staleTime; - final Duration _cacheTime; - - // all properties - T? data; - dynamic error; - QueryStatus status; + Duration _staleTime; /// total count of how many times the query retried to get a successful /// result - int retryAttempts = 0; - DateTime updatedAt; int refetchCount = 0; bool enabled; - @protected - bool fetched = false; - @protected final Set> onDataListeners = Set>(); @protected @@ -60,32 +60,37 @@ class Query extends ChangeNotifier { Outside? _prevUsedExternalData; - /// used for keeping track of query activity. If the are no mounts & - /// the passed cached time is over than the query is removed from - /// storage/cache - Set> _mounts = {}; + Duration? refetchInterval; + + Timer? _refetchIntervalTimer; Query({ required this.queryKey, required this.task, required Duration staleTime, - required Duration cacheTime, + required super.cacheTime, required Outside externalData, - required this.retries, - required this.retryDelay, - T? initialData, + required super.retries, + required super.retryDelay, + this.refetchOnMount, + this.refetchInterval, this.enabled = true, + T? initialData, QueryListener? onData, QueryListener? onError, - }) : status = QueryStatus.pending, - _staleTime = staleTime, - _cacheTime = cacheTime, + }) : _staleTime = staleTime, _initialData = initialData, _externalData = externalData, - data = initialData, - updatedAt = DateTime.now() { + super( + status: QueryStatus.idle, + data: initialData, + ) { if (onData != null) onDataListeners.add(onData); if (onError != null) onErrorListeners.add(onError); + + if (refetchInterval != null && refetchInterval != Duration.zero) { + _refetchIntervalTimer = _createRefetchTimer(); + } } Query.fromOptions( @@ -96,50 +101,34 @@ class Query extends ChangeNotifier { }) : queryKey = options.queryKey, enabled = options.enabled ?? true, task = options.task, - retries = options.retries ?? 3, - retryDelay = options.retryDelay ?? const Duration(milliseconds: 200), _staleTime = options.staleTime ?? const Duration(milliseconds: 500), - _cacheTime = options.cacheTime ?? const Duration(minutes: 5), _initialData = options.initialData, _externalData = externalData, - data = options.initialData, - status = QueryStatus.pending, - updatedAt = DateTime.now() { + refetchInterval = options.refetchInterval, + refetchOnMount = options.refetchOnMount, + super( + status: QueryStatus.idle, + 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); } // all getters & setters - bool get hasData => data != null && error == null; - bool get hasError => - status == QueryStatus.failed && error != null && data == null; - bool get isLoading => - status == QueryStatus.pending && data == null && error == null; - bool get isRefetching => - status == QueryStatus.refetching && (data != null || error != null); - bool get isSucceeded => status == QueryStatus.succeed && data != null; - bool get isIdle => isSucceeded && error == null; - bool get isInactive => _mounts.isEmpty; + Outside get externalData => _externalData; Outside? get prevUsedExternalData => _prevUsedExternalData; - // all methods - - void mount(ValueKey uKey) { - _mounts.add(uKey); - } - - void unmount(ValueKey uKey) { - if (_mounts.length == 1) { - Future.delayed(_cacheTime, () { - _mounts.remove(uKey); - // for letting know QueryBowl that this one's time has come for - // getting crushed - notifyListeners(); - }); - } else { - _mounts.remove(uKey); - } + Timer _createRefetchTimer() { + return Timer.periodic( + refetchInterval!, + (_) async { + if (isStale) await refetch(); + }, + ); } /// Calls the task function & doesn't check if there's already @@ -150,14 +139,14 @@ class Query extends ChangeNotifier { data = await task(queryKey, _externalData); _prevUsedExternalData = _externalData; updatedAt = DateTime.now(); - status = QueryStatus.succeed; + status = QueryStatus.success; for (final onData in onDataListeners) { onData(data!); } notifyListeners(); } catch (e) { if (retries == 0) { - status = QueryStatus.failed; + status = QueryStatus.error; error = e; for (final onError in onErrorListeners) { onError(error); @@ -170,18 +159,18 @@ class Query extends ChangeNotifier { try { data = await task(queryKey, _externalData); _prevUsedExternalData = _externalData; - status = QueryStatus.succeed; + status = QueryStatus.success; for (final onData in onDataListeners) { - onData(data!); + await onData(data!); } notifyListeners(); break; } catch (e) { if (retryAttempts == retries) { - status = QueryStatus.failed; + status = QueryStatus.error; error = e; for (final onError in onErrorListeners) { - onError(error); + await onError(error); } notifyListeners(); } @@ -193,12 +182,12 @@ class Query extends ChangeNotifier { } Future fetch() async { - status = QueryStatus.pending; - notifyListeners(); if (!enabled) return null; - if (!isStale && hasData) { + if (hasData) { return data; } + status = QueryStatus.loading; + notifyListeners(); return _execute().then((_) { fetched = true; return data; @@ -234,11 +223,12 @@ class Query extends ChangeNotifier { "[fl_query] new instance of data should be returned because of immutability"); } data = newData; - status = QueryStatus.succeed; + status = QueryStatus.success; notifyListeners(); } setExternalData(Outside externalData) { + _prevUsedExternalData = _externalData; _externalData = externalData; } @@ -247,19 +237,58 @@ class Query extends ChangeNotifier { data = _initialData; error = null; fetched = false; - status = QueryStatus.pending; + status = QueryStatus.idle; retryAttempts = 0; onDataListeners.clear(); onErrorListeners.clear(); - _mounts.clear(); + mounts.clear(); + } + + /// Update configurations of the query after already creating the Query + /// instance + void updateDefaultOptions({ + Duration? refetchInterval, + Duration? staleTime, + Duration? cacheTime, + bool? refetchOnMount, + }) { + if (this.refetchInterval == null && + refetchInterval != null && + refetchInterval != Duration.zero) { + this.refetchInterval = refetchInterval; + _refetchIntervalTimer?.cancel(); + _refetchIntervalTimer = _createRefetchTimer(); + } + if (this.cacheTime == Duration(minutes: 5) && cacheTime != null) + this.cacheTime = cacheTime; + if (this._staleTime == const Duration(milliseconds: 500) && + staleTime != null) this._staleTime = staleTime; + if (this.refetchOnMount == null && refetchOnMount != null) + this.refetchOnMount = refetchOnMount; + 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 // the data has become stale return DateTime.now().isAfter(updatedAt.add(_staleTime)); } + @override + bool get isError => status == QueryStatus.error; + @override + bool get isIdle => status == QueryStatus.idle; + @override + bool get isLoading => status == QueryStatus.loading; + bool get isRefetching => status == QueryStatus.refetching; + @override + bool get isSuccess => status == QueryStatus.success; + A? cast() => this is A ? this as A : null; String get debugLabel => "Query($queryKey)"; diff --git a/packages/fl_query/lib/query_bowl.dart b/packages/fl_query/lib/query_bowl.dart index 8805e38..4268c44 100644 --- a/packages/fl_query/lib/query_bowl.dart +++ b/packages/fl_query/lib/query_bowl.dart @@ -4,6 +4,7 @@ import 'package:fl_query/models/query_job.dart'; import 'package:fl_query/mutation.dart'; import 'package:fl_query/query.dart'; import 'package:collection/collection.dart'; +import 'package:fl_query/utils.dart'; import 'package:flutter/widgets.dart'; class QueryBowlScope extends StatefulWidget { @@ -11,14 +12,30 @@ class QueryBowlScope extends StatefulWidget { final Duration staleTime; final Duration cacheTime; + // refetching options + + // refetch query when new query instance mounts + final bool refetchOnMount; + // for desktop & web only + final bool refetchOnWindowFocus; + // for mobile only + final bool refetchOnApplicationResume; + // refetch when user's device reconnects to the internet after no being + // connected before + final bool refetchOnReconnect; + /// used for periodically checking if any query got stale. /// If none is supplied then half of the value of staleTime is used - final Duration refreshInterval; + final Duration refetchInterval; const QueryBowlScope({ required this.child, - this.staleTime = const Duration(milliseconds: 500), + this.staleTime = Duration.zero, this.cacheTime = const Duration(minutes: 5), - this.refreshInterval = const Duration(minutes: 5), + this.refetchInterval = Duration.zero, + this.refetchOnMount = false, + this.refetchOnReconnect = true, + this.refetchOnApplicationResume = true, + this.refetchOnWindowFocus = true, Key? key, }) : super(key: key); @@ -30,34 +47,19 @@ class _QueryBowlScopeState extends State { late Set queries; late Set mutations; - late Timer refreshIntervalTimer; - @override void initState() { super.initState(); queries = {}; mutations = {}; - refreshIntervalTimer = Timer.periodic( - widget.refreshInterval, - _checkAndUpdateStaleQueriesOnBg, - ); } @override void dispose() { - refreshIntervalTimer.cancel(); _disposeUpdateListeners(); super.dispose(); } - Future _checkAndUpdateStaleQueriesOnBg([dynamic _]) async { - // checking for staled queries inside the widget as InheritedWidget - // classes has to be constant & doesn't this kind of dynamic behavior - for (final query in queries) { - if (query.isStale) await query.refetch(); - } - } - void _listenToUpdates() { for (final query in queries) { query.addListener(() => updateQueries(query)); @@ -147,6 +149,9 @@ class _QueryBowlScopeState extends State { queries: queries, mutations: mutations, staleTime: widget.staleTime, + cacheTime: widget.cacheTime, + refetchInterval: widget.refetchInterval, + refetchOnMount: widget.refetchOnMount, child: widget.child, ); } @@ -158,6 +163,10 @@ class QueryBowl extends InheritedWidget { final Set _queries; final Set _mutations; final Duration staleTime; + final Duration cacheTime; + + final Duration? refetchInterval; + final bool refetchOnMount; final void Function(Query query) _addQuery; @@ -179,8 +188,11 @@ class QueryBowl extends InheritedWidget { required final Set queries, required final Set mutations, required this.staleTime, + required this.cacheTime, required this.removeQueries, required this.clear, + required this.refetchOnMount, + this.refetchInterval, Key? key, }) : _addQuery = addQuery, _queries = queries, @@ -201,8 +213,12 @@ class QueryBowl extends InheritedWidget { if (prevQuery is Query) { // run the query if its still not called or if externalData has // changed - final hasExternalDataChanged = - prevQuery.prevUsedExternalData != externalData; + final hasExternalDataChanged = prevQuery.prevUsedExternalData != null && + externalData != null && + !isShallowEqual( + prevQuery.prevUsedExternalData!, + externalData, + ); prevQuery.mount(key); if (onData != null) prevQuery.onDataListeners.add(onData); if (onError != null) prevQuery.onErrorListeners.add(onError); @@ -215,6 +231,12 @@ class QueryBowl extends InheritedWidget { // mounting the widget that is using the query in the prevQuery return prevQuery.data; } + + /// populating with default configurations + options.refetchInterval ??= refetchInterval; + options.staleTime ??= staleTime; + options.cacheTime ??= cacheTime; + options.refetchOnMount ??= refetchOnMount; final query = Query.fromOptions( options, externalData: externalData, @@ -238,8 +260,14 @@ class QueryBowl extends InheritedWidget { if (prevQuery is Query) { // run the query if its still not called or if externalData has // changed - if (prevQuery.prevUsedExternalData != query.externalData) + if (prevQuery.prevUsedExternalData != null && + query.externalData != null && + !isShallowEqual( + prevQuery.prevUsedExternalData!, + query.externalData!, + )) { prevQuery.setExternalData(query.externalData); + } prevQuery.mount(key); if (onData != null) prevQuery.onDataListeners.add(onData); if (onError != null) prevQuery.onErrorListeners.add(onError); @@ -248,6 +276,12 @@ class QueryBowl extends InheritedWidget { } if (onData != null) query.onDataListeners.add(onData); if (onError != null) query.onErrorListeners.add(onError); + query.updateDefaultOptions( + cacheTime: cacheTime, + staleTime: staleTime, + refetchInterval: refetchInterval, + refetchOnMount: refetchOnMount, + ); query.mount(key); _addQuery(query); return query; @@ -270,6 +304,7 @@ class QueryBowl extends InheritedWidget { prevMutation.mount(key); return prevMutation; } else { + mutation.updateDefaultOptions(cacheTime: cacheTime); mutation.mount(key); _addMutation(mutation); return mutation; diff --git a/packages/fl_query/lib/query_builder.dart b/packages/fl_query/lib/query_builder.dart index b18c4b0..caf37c1 100644 --- a/packages/fl_query/lib/query_builder.dart +++ b/packages/fl_query/lib/query_builder.dart @@ -51,13 +51,20 @@ class _QueryBuilderState onData: widget.onData, onError: widget.onError, ); - await query.fetch(); + final hasExternalDataChanged = query.externalData != null && + query.prevUsedExternalData != null && + !isShallowEqual(query.externalData!, query.prevUsedExternalData!); + (query.fetched && query.refetchOnMount == true) || hasExternalDataChanged + ? await query.refetch() + : await query.fetch(); }); } @override void didUpdateWidget(covariant oldWidget) { - if (oldWidget.externalData != widget.externalData) { + if (oldWidget.externalData != null && + widget.externalData != null && + !isShallowEqual(oldWidget.externalData!, widget.externalData!)) { QueryBowl.of(context).fetchQuery( widget.job, externalData: widget.externalData, diff --git a/packages/fl_query/lib/utils.dart b/packages/fl_query/lib/utils.dart index 61ed734..b68b46a 100644 --- a/packages/fl_query/lib/utils.dart +++ b/packages/fl_query/lib/utils.dart @@ -11,3 +11,33 @@ Future callQueryListeners(Set> listeners, T data) { } const uuid = Uuid(); + +bool isShallowEqualList(List list1, List list2) { + return list1.asMap().entries.every((l1Entry) { + return l1Entry.value == list2[l1Entry.key]; + }); +} + +bool isShallowEqualSet(Set list1, Set list2) { + return isShallowEqualList(list1.toList(), list2.toList()); +} + +bool isShallowEqualMap(Map list1, Map list2) { + return list1.entries.every((l1Entry) { + return l1Entry.value == list2[l1Entry.key]; + }); +} + +bool isShallowEqual(Object obj1, Object obj2) { + if (obj1 is List && obj2 is List) { + return isShallowEqualList(obj1, obj2); + } else if (obj1 is Set && obj2 is Set) { + return isShallowEqualSet(obj1, obj2); + } else if (obj1 is Map && obj2 is Map) { + return isShallowEqualMap(obj1, obj2); + } else { + // for other types basically comparing references for non primitive + // types. And primitives are always compared by value + return obj1 == obj2; + } +} diff --git a/packages/fl_query/test/query_test.dart b/packages/fl_query/test/query_test.dart index fdc1bc7..4456980 100644 --- a/packages/fl_query/test/query_test.dart +++ b/packages/fl_query/test/query_test.dart @@ -30,12 +30,12 @@ void main() { expect(query.retryAttempts, 0); expect(query.fetched, isFalse); expect(query.retryDelay, Duration(milliseconds: 200)); - expect(query.status, QueryStatus.pending); + expect(query.status, QueryStatus.loading); expect(query.isStale, isFalse); expect(query.isIdle, isFalse); expect(query.isInactive, isTrue); expect(query.isLoading, isTrue); - expect(query.isSucceeded, isFalse); + expect(query.isSuccess, isFalse); expect(query.hasError, isFalse); expect(query.hasData, isFalse); });