diff --git a/packages/fl_query/.flutter-plugins-dependencies b/packages/fl_query/.flutter-plugins-dependencies index 456c8fd..ca26f71 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-08-16 21:50:29.137318","version":"3.0.5"} \ 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-08-24 21:03:29.720419","version":"3.0.5"} \ No newline at end of file diff --git a/packages/fl_query/example/.flutter-plugins-dependencies b/packages/fl_query/example/.flutter-plugins-dependencies index 65d57b1..bc68288 100644 --- a/packages/fl_query/example/.flutter-plugins-dependencies +++ b/packages/fl_query/example/.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-08-21 19:29:44.415263","version":"3.0.5"} \ 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-08-24 21:03:52.762936","version":"3.0.5"} \ No newline at end of file diff --git a/packages/fl_query/lib/src/base_operation.dart b/packages/fl_query/lib/src/base_operation.dart index 20d532f..03978d8 100644 --- a/packages/fl_query/lib/src/base_operation.dart +++ b/packages/fl_query/lib/src/base_operation.dart @@ -1,7 +1,7 @@ import 'package:fl_query/src/query_bowl.dart'; import 'package:flutter/widgets.dart'; -abstract class BaseOperation extends ChangeNotifier { +abstract class BaseOperation extends ChangeNotifier { /// The number of times the query should refetch in the time of error /// before giving up final int retries; @@ -13,7 +13,7 @@ abstract class BaseOperation extends ChangeNotifier { // all properties Data? data; - dynamic error; + Error? error; /// total count of how many times the query retried to get a successful /// result diff --git a/packages/fl_query/lib/src/base_query.dart b/packages/fl_query/lib/src/base_query.dart new file mode 100644 index 0000000..6e99cce --- /dev/null +++ b/packages/fl_query/lib/src/base_query.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:fl_query/src/base_operation.dart'; +import 'package:fl_query/src/mixins/autocast.dart'; +import 'package:fl_query/src/query.dart'; +import 'package:fl_query/src/utils.dart'; +import 'package:flutter/widgets.dart'; + +abstract class BaseQuery + extends BaseOperation with AutoCast { +// all params + final String queryKey; + bool? refetchOnMount; + bool? refetchOnReconnect; + + final T? _initialData; + + // got from global options + Duration _staleTime; + + /// total count of how many times the query retried to get a successful + /// result + int refetchCount = 0; + bool enabled; + + QueryStatus status; + + final Set _onDataListeners = Set(); + final Set _onErrorListeners = Set(); + + // externalData will always be passed to the task Callback + // it will change based on the presence of QueryBuilder + Outside _externalData; + + Outside? _prevUsedExternalData; + + Duration? refetchInterval; + + Timer? _refetchIntervalTimer; + + Connectivity _connectivity; + + T? _previousData; + + BaseQuery({ + required this.queryKey, + required Duration staleTime, + required super.cacheTime, + required Outside externalData, + required super.retries, + required super.retryDelay, + required super.queryBowl, + required this.status, + this.refetchOnMount, + this.refetchOnReconnect, + this.refetchInterval, + this.enabled = true, + T? previousData, + Connectivity? connectivity, + T? initialData, + onData, + onError, + }) : _staleTime = staleTime, + _initialData = initialData, + _externalData = externalData, + _connectivity = connectivity ?? Connectivity(), + _previousData = previousData, + super(data: previousData ?? initialData) { + if (onData != null) _onDataListeners.add(onData); + if (onError != null) _onErrorListeners.add(onError); + + if (refetchInterval != null && refetchInterval != Duration.zero) { + _refetchIntervalTimer = _createRefetchTimer(); + } + } + // all getters & setters + + Outside get externalData => _externalData; + Outside? get prevUsedExternalData => _prevUsedExternalData; + + Timer _createRefetchTimer() { + return Timer.periodic( + refetchInterval!, + (_) async { + // only refetch if its connected to the internet or refetch will + // always result in error while there's no internet + if (isStale && await isInternetConnected()) await refetch(); + }, + ); + } + + /// Calls the task function & doesn't check if there's already + /// cached data available + @protected + Future execute() async { + try { + retryAttempts = 0; + await setData(); + _prevUsedExternalData = _externalData; + updatedAt = DateTime.now(); + status = QueryStatus.success; + for (final onData in _onDataListeners) { + onData(data!); + } + notifyListeners(); + } catch (e) { + if (retries == 0) { + status = QueryStatus.error; + setError(e); + for (final onError in _onErrorListeners) { + onError(error); + } + notifyListeners(); + } else { + // retrying for retry count if failed for the first time + while (retryAttempts <= retries) { + await Future.delayed(retryDelay); + try { + await setData(); + _prevUsedExternalData = _externalData; + status = QueryStatus.success; + for (final onData in _onDataListeners) { + await onData(data!); + } + notifyListeners(); + break; + } catch (e) { + if (retryAttempts == retries) { + status = QueryStatus.error; + setError(e); + for (final onError in _onErrorListeners) { + await onError(error); + } + notifyListeners(); + break; + } + retryAttempts++; + } + } + } + } + } + + void addDataListener(listener) { + _onDataListeners.add(listener); + } + + void addErrorListener(listener) { + _onErrorListeners.add(listener); + } + + void removeDataListener(listener) { + _onDataListeners.remove(listener); + } + + void removeErrorListener(listener) { + _onErrorListeners.remove(listener); + } + + /// fetches data or runs the provided task initially + /// + /// Once [data] is available it won't run the [task] ever again + /// and will only return the available data + /// + /// If a [fetch] is already running in the background it'll just return + /// the current available [data] (which can be nul if no [initialPage] + /// was provided) instead of running the task to prevent race conditions + Future fetch() async { + if (!enabled) return null; + + /// if isLoading/isRefetching is true that means its already fetching/ + /// refetching. So [_execute] again can create a race condition + if (isLoading || isRefetching || (hasData && !isPreviousData)) return data; + status = QueryStatus.loading; + notifyListeners(); + return execute().then((_) { + fetched = true; + return data; + }); + } + + /// refetches a valid or invalid [Query] + /// + /// When called before calling [fetch] in a [Query] it'll + /// automatically run [fetch] + /// + /// But if it's used to fetch the first data of a non-enabled [Query] + /// aka `LazyQuery`, it'll execute the task & will set the status + /// `enabled=true` + /// + /// If a [refetch] is already running in the background it'll just return + /// the current available [data] instead of running the task to prevent + /// race conditions + Future refetch() async { + /// if isLoading/isRefetching is true that means its already fetching/ + /// refetching. So [_execute] again can create a race condition + if (isRefetching || isLoading) return data; + if (enabled && !fetched) return await fetch(); + status = QueryStatus.refetching; + refetchCount++; + // disabling the lazy query bound when query was actually called + if (!enabled) enabled = true; + notifyListeners(); + return await execute().then((_) => data); + } + + FutureOr setData(); + void setError(dynamic); + + /// Sets the [externalData] from outside of the query + /// + /// Remember, it's for the very instance of [Query] + /// So this won't persist through later UI/[Query] updates + void setExternalData(Outside externalData) { + _prevUsedExternalData = _externalData; + _externalData = externalData; + } + + /// Resets the query + /// + /// The values of internal state of the query are reset to the + /// initial ones + void reset() { + refetchCount = 0; + data = _previousData ?? _initialData; + error = null; + fetched = false; + status = QueryStatus.idle; + retryAttempts = 0; + _onDataListeners.clear(); + _onErrorListeners.clear(); + mounts.clear(); + } + + /// Update configurations of the query + /// after already creating the Query instance + /// + /// Remember, it's just for the single query instance + /// In the next UI update/render the options will get reset + /// to the default ones defined in the [QueryJob] or [QueryBowlScope] + void updateDefaultOptions({ + Duration? refetchInterval, + Duration? staleTime, + Duration? cacheTime, + bool? refetchOnMount, + bool? refetchOnReconnect, + }) { + 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; + if (this.refetchOnReconnect == null && refetchOnReconnect != null) + this.refetchOnReconnect = refetchOnReconnect; + notifyListeners(); + } + + /// checks if the application is connected to internet in any mean + /// + /// It's true when any one this is connected - + /// - ethernet + /// - mobile + /// - wifi + Future isInternetConnected() async { + return isConnectedToInternet(await _connectivity.checkConnectivity()); + } + + /// invalidates the query + /// + /// Forcefully makes the query stale & expired which results in a refetch + /// when met conditions + 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 [DateTime.now()] is after [update_at + stale_time] it means + // the data has become stale + return DateTime.now().isAfter(updatedAt.add(_staleTime)); + } + + bool get isError => status == QueryStatus.error; + bool get isIdle => status == QueryStatus.idle; + bool get isLoading => status == QueryStatus.loading; + bool get isRefetching => status == QueryStatus.refetching; + bool get isSuccess => status == QueryStatus.success; + bool get isPreviousData { + return _previousData != null ? _previousData == data : false; + } + + String get debugLabel; + + @override + String toString() { + return debugLabel; + } + + operator ==(other); +} diff --git a/packages/fl_query/lib/src/infinite_query.dart b/packages/fl_query/lib/src/infinite_query.dart index 58fb654..e8b323c 100644 --- a/packages/fl_query/lib/src/infinite_query.dart +++ b/packages/fl_query/lib/src/infinite_query.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'package:fl_query/src/mixins/autocast.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:fl_query/src/base_query.dart'; import 'package:fl_query/src/models/infinite_query_job.dart'; -import 'package:flutter/widgets.dart'; +import 'package:fl_query/src/query.dart'; typedef InfiniteQueryTaskFunction @@ -16,84 +17,94 @@ typedef InfiniteQueryPageParamFunction Function(T lastPage, PageParam lastParam); class InfiniteQuery - with ChangeNotifier, AutoCast { - String queryKey; - - Map _data; - Map _error; - + extends BaseQuery, Outside, Map> { InfiniteQueryTaskFunction task; InfiniteQueryPageParamFunction? getNextPageParam; InfiniteQueryPageParamFunction? getPreviousPageParam; - List get pageParams => _data.keys.toList(); - List get errors => _error.values.toList(); - List get pages => _data.values.toList(); - - PageParam currentParam; - - Outside _externalData; - bool _hasNextPage = false; bool _hasPreviousPage = false; + bool _isFetchingNextPage = false; + bool _isFetchingPreviousPage = false; + + bool get isFetchingNextPage => _isFetchingNextPage; + bool get isFetchingPreviousPage => _isFetchingPreviousPage; bool get hasNextPage => _hasNextPage; bool get hasPreviousPage => _hasPreviousPage; + PageParam _currentParam; + InfiniteQuery({ - required this.queryKey, + required super.queryKey, required this.task, + required super.staleTime, + required super.cacheTime, + required super.externalData, + required super.retries, + required super.retryDelay, + required super.queryBowl, + required super.status, required PageParam initialParam, - required Outside externalData, + super.refetchOnMount, + super.refetchOnReconnect, + super.refetchInterval, + super.enabled, + super.previousData, + super.connectivity, + super.onData, + super.onError, + required T? initialPage, this.getNextPageParam, this.getPreviousPageParam, - T? initialPage, - }) : currentParam = initialParam, - _externalData = externalData, - _error = {}, - _data = { - if (initialPage != null) initialParam: initialPage, - }; + }) : _currentParam = initialParam, + super(initialData: {initialParam: initialPage}); InfiniteQuery.fromOptions( InfiniteQueryJob options, { + required super.queryBowl, required Outside externalData, - }) : queryKey = options.queryKey, - task = options.task, - currentParam = options.initialParam, - _externalData = externalData, - _error = {}, + QueryListener? onData, + QueryListener? onError, + }) : task = options.task, + _currentParam = options.initialParam, getNextPageParam = options.getNextPageParam, getPreviousPageParam = options.getPreviousPageParam, - _data = { - if (options.initialPage != null) - options.initialParam: options.initialPage, - }; + super( + cacheTime: options.cacheTime ?? const Duration(minutes: 5), + retries: options.retries ?? 3, + retryDelay: options.retryDelay ?? const Duration(milliseconds: 200), + externalData: externalData, + enabled: options.enabled ?? true, + staleTime: options.staleTime ?? const Duration(milliseconds: 500), + refetchInterval: options.refetchInterval, + refetchOnMount: options.refetchOnMount, + refetchOnReconnect: options.refetchOnReconnect, + status: QueryStatus.idle, + connectivity: options.connectivity ?? Connectivity(), + queryKey: options.queryKey, + initialData: {options.initialParam: options.initialPage}, + ); - Future _execute() async { - final page = await task( - queryKey, - currentParam, - _externalData, - ); - _data[currentParam] = page; - notifyListeners(); - } - - Future> fetch() async { - if (_data.isEmpty) await _execute(); - return pages; - } + List get pageParams => data?.keys.toList() ?? []; + List get errors => error?.values.toList() ?? []; + List get pages => data?.values.toList() ?? []; Future fetchNextPage([ InfiniteQueryPageParamFunction? getNextPageParam, ]) async { try { - if (_data[currentParam] == null) await _execute(); + if (isFetchingNextPage || + isFetchingPreviousPage || + isLoading || + isRefetching) return null; + if (data == null || data?[_currentParam] == null) execute(); + _isFetchingNextPage = true; + _isFetchingPreviousPage = false; final nextParam = await (getNextPageParam ?? this.getNextPageParam)?.call( - _data[currentParam]!, - currentParam, + data![_currentParam]!, + _currentParam, ); if (nextParam == null) { _hasNextPage = false; @@ -101,11 +112,11 @@ class InfiniteQuery return null; } _hasNextPage = true; - currentParam = nextParam; - return await _execute().then((_) => _data[currentParam]); - } catch (e) { - print("[InfiniteQuery.fetchNextPage]: $e"); - rethrow; + _currentParam = nextParam; + return await refetch().then((data) => data?[_currentParam]); + } finally { + _isFetchingNextPage = false; + notifyListeners(); } } @@ -113,11 +124,18 @@ class InfiniteQuery InfiniteQueryPageParamFunction? getPreviousPageParam, ]) async { try { - if (_data[currentParam] == null) await _execute(); + if (isFetchingNextPage || + isFetchingPreviousPage || + isLoading || + isRefetching) return null; + _isFetchingPreviousPage = true; + _isFetchingNextPage = false; + notifyListeners(); + if (data?[_currentParam] == null) execute(); final prevParam = - await (getNextPageParam ?? this.getPreviousPageParam)?.call( - _data[currentParam]!, - currentParam, + await (getPreviousPageParam ?? this.getPreviousPageParam)?.call( + data![_currentParam]!, + _currentParam, ); if (prevParam == null) { _hasPreviousPage = false; @@ -125,11 +143,44 @@ class InfiniteQuery return null; } _hasPreviousPage = true; - currentParam = prevParam; - return await _execute().then((_) => _data[currentParam]); + _currentParam = prevParam; + return await refetch().then((_) => data?[_currentParam]); } catch (e) { print("[InfiniteQuery.fetchPreviousPage]: $e"); rethrow; + } finally { + _isFetchingPreviousPage = false; + notifyListeners(); } } + + @override + // TODO: implement debugLabel + String get debugLabel => "InfiniteQuery($queryKey)"; + + @override + void setData() async { + if (data == null) data = Map(); + data?[_currentParam] = await task( + queryKey, + _currentParam, + externalData, + ); + } + + @override + void setError(specError) { + if (error is! Map) error = Map(); + error?[_currentParam] = specError; + } + + @override + bool operator ==(other) { + return (other is InfiniteQuery && + other.queryKey == queryKey) || + identical(other, this); + } + + @override + bool get hasData => data?[_currentParam] != null; } diff --git a/packages/fl_query/lib/src/infinite_query_builder.dart b/packages/fl_query/lib/src/infinite_query_builder.dart index 3f1519e..64ab2c8 100644 --- a/packages/fl_query/lib/src/infinite_query_builder.dart +++ b/packages/fl_query/lib/src/infinite_query_builder.dart @@ -45,14 +45,15 @@ class _InfiniteQueryBuilderState = FutureOr Function( typedef MutationTaskFunction = FutureOr Function( String queryKey, V variables); -class Mutation extends BaseOperation with AutoCast { +class Mutation extends BaseOperation + with AutoCast { // all params final String mutationKey; MutationTaskFunction task; diff --git a/packages/fl_query/lib/src/query.dart b/packages/fl_query/lib/src/query.dart index 5d72dbc..3101db1 100644 --- a/packages/fl_query/lib/src/query.dart +++ b/packages/fl_query/lib/src/query.dart @@ -1,10 +1,9 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:fl_query/src/base_operation.dart'; -import 'package:fl_query/src/mixins/autocast.dart'; +import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/src/base_query.dart'; import 'package:fl_query/src/models/query_job.dart'; -import 'package:fl_query/src/utils.dart'; import 'package:flutter/widgets.dart'; enum QueryStatus { @@ -37,76 +36,29 @@ typedef ListenerUnsubscriber = void Function(); typedef QueryUpdateFunction = FutureOr Function(T? oldData); -class Query extends BaseOperation with AutoCast { - // all params - final String queryKey; +class Query extends BaseQuery { QueryTaskFunction task; - bool? refetchOnMount; - bool? refetchOnReconnect; - - final T? _initialData; - - // got from global options - Duration _staleTime; - - /// total count of how many times the query retried to get a successful - /// result - int refetchCount = 0; - bool enabled; - - QueryStatus status; - - final Set> _onDataListeners = Set>(); - final Set> _onErrorListeners = - Set>(); - - // externalData will always be passed to the task Callback - // it will change based on the presence of QueryBuilder - Outside _externalData; - - Outside? _prevUsedExternalData; - - Duration? refetchInterval; - - Timer? _refetchIntervalTimer; - - Connectivity _connectivity; - - T? _previousData; - Query({ - required this.queryKey, + required super.queryKey, required this.task, - required Duration staleTime, + required super.staleTime, required super.cacheTime, - required Outside externalData, + required super.externalData, required super.retries, required super.retryDelay, required super.queryBowl, - this.refetchOnMount, - this.refetchOnReconnect, - this.refetchInterval, - this.enabled = true, - T? previousData, - Connectivity? connectivity, - T? initialData, - QueryListener? onData, - QueryListener? onError, - }) : _staleTime = staleTime, - _initialData = initialData, - _externalData = externalData, - status = previousData == null ? QueryStatus.idle : QueryStatus.success, - _connectivity = connectivity ?? Connectivity(), - _previousData = previousData, - super(data: previousData ?? initialData) { - if (onData != null) _onDataListeners.add(onData); - if (onError != null) _onErrorListeners.add(onError); - - if (refetchInterval != null && refetchInterval != Duration.zero) { - _refetchIntervalTimer = _createRefetchTimer(); - } - } + required super.status, + super.refetchOnMount, + super.refetchOnReconnect, + super.refetchInterval, + super.enabled, + super.previousData, + super.connectivity, + super.initialData, + super.onData, + super.onError, + }); Query.fromOptions( QueryJob options, { @@ -115,166 +67,23 @@ class Query extends BaseOperation with AutoCast { T? previousData, QueryListener? onData, QueryListener? onError, - }) : queryKey = options.queryKey, - enabled = options.enabled ?? true, - task = options.task, - _staleTime = options.staleTime ?? const Duration(milliseconds: 500), - _initialData = options.initialData, - _externalData = externalData, - refetchInterval = options.refetchInterval, - refetchOnMount = options.refetchOnMount, - refetchOnReconnect = options.refetchOnReconnect, - status = previousData == null ? QueryStatus.idle : QueryStatus.success, - _connectivity = options.connectivity ?? Connectivity(), - _previousData = previousData, + }) : task = options.task, super( cacheTime: options.cacheTime ?? const Duration(minutes: 5), retries: options.retries ?? 3, retryDelay: options.retryDelay ?? const Duration(milliseconds: 200), - data: previousData ?? options.initialData, - ) { - if (onData != null) _onDataListeners.add(onData); - if (onError != null) _onErrorListeners.add(onError); - if (refetchInterval != null && refetchInterval != Duration.zero) { - _refetchIntervalTimer = _createRefetchTimer(); - } - } - - // all getters & setters - - Outside get externalData => _externalData; - Outside? get prevUsedExternalData => _prevUsedExternalData; - - Timer _createRefetchTimer() { - return Timer.periodic( - refetchInterval!, - (_) async { - // only refetch if its connected to the internet or refetch will - // always result in error while there's no internet - if (isStale && await isInternetConnected()) await refetch(); - }, - ); - } - - /// Calls the task function & doesn't check if there's already - /// cached data available - Future _execute() async { - try { - retryAttempts = 0; - data = await task( - queryKey, - _externalData, - ); - _prevUsedExternalData = _externalData; - updatedAt = DateTime.now(); - status = QueryStatus.success; - for (final onData in _onDataListeners) { - onData(data!); - } - notifyListeners(); - } catch (e) { - if (retries == 0) { - status = QueryStatus.error; - error = e; - for (final onError in _onErrorListeners) { - onError(error); - } - notifyListeners(); - } else { - // retrying for retry count if failed for the first time - while (retryAttempts <= retries) { - await Future.delayed(retryDelay); - try { - data = await task( - queryKey, - _externalData, - ); - _prevUsedExternalData = _externalData; - status = QueryStatus.success; - for (final onData in _onDataListeners) { - await onData(data!); - } - notifyListeners(); - break; - } catch (e) { - if (retryAttempts == retries) { - status = QueryStatus.error; - error = e; - for (final onError in _onErrorListeners) { - await onError(error); - } - notifyListeners(); - break; - } - retryAttempts++; - } - } - } - } - } - - 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); - } - - /// fetches data or runs the provided task initially - /// - /// Once [data] is available it won't run the [task] ever again - /// and will only return the available data - /// - /// If a [fetch] is already running in the background it'll just return - /// the current available [data] (which can be nul if no [initialPage] - /// was provided) instead of running the task to prevent race conditions - Future fetch() async { - if (!enabled) return null; - - /// if isLoading/isRefetching is true that means its already fetching/ - /// refetching. So [_execute] again can create a race condition - if (isLoading || isRefetching || (hasData && !isPreviousData)) return data; - status = QueryStatus.loading; - notifyListeners(); - return _execute().then((_) { - fetched = true; - return data; - }); - } - - /// refetches a valid or invalid [Query] - /// - /// When called before calling [fetch] in a [Query] it'll - /// automatically run [fetch] - /// - /// But if it's used to fetch the first data of a non-enabled [Query] - /// aka `LazyQuery`, it'll execute the task & will set the status - /// `enabled=true` - /// - /// If a [refetch] is already running in the background it'll just return - /// the current available [data] instead of running the task to prevent - /// race conditions - Future refetch() async { - /// if isLoading/isRefetching is true that means its already fetching/ - /// refetching. So [_execute] again can create a race condition - if (isRefetching || isLoading) return data; - if (enabled && !fetched) return await fetch(); - status = QueryStatus.refetching; - refetchCount++; - // disabling the lazy query bound when query was actually called - if (!enabled) enabled = true; - notifyListeners(); - return await _execute().then((_) => data); - } + externalData: externalData, + enabled: options.enabled ?? true, + staleTime: options.staleTime ?? const Duration(milliseconds: 500), + initialData: options.initialData, + refetchInterval: options.refetchInterval, + refetchOnMount: options.refetchOnMount, + refetchOnReconnect: options.refetchOnReconnect, + status: previousData == null ? QueryStatus.idle : QueryStatus.success, + connectivity: options.connectivity ?? Connectivity(), + previousData: previousData, + queryKey: options.queryKey, + ); /// can be used to update the data manually. Can be useful when used /// together with mutations to perform optimistic updates or manual data @@ -293,105 +102,6 @@ class Query extends BaseOperation with AutoCast { notifyListeners(); } - /// Sets the [externalData] from outside of the query - /// - /// Remember, it's for the very instance of [Query] - /// So this won't persist through later UI/[Query] updates - void setExternalData(Outside externalData) { - _prevUsedExternalData = _externalData; - _externalData = externalData; - } - - /// Resets the query - /// - /// The values of internal state of the query are reset to the - /// initial ones - void reset() { - refetchCount = 0; - data = _previousData ?? _initialData; - error = null; - fetched = false; - status = QueryStatus.idle; - retryAttempts = 0; - _onDataListeners.clear(); - _onErrorListeners.clear(); - mounts.clear(); - } - - /// Update configurations of the query - /// after already creating the Query instance - /// - /// Remember, it's just for the single query instance - /// In the next UI update/render the options will get reset - /// to the default ones defined in the [QueryJob] or [QueryBowlScope] - void updateDefaultOptions({ - Duration? refetchInterval, - Duration? staleTime, - Duration? cacheTime, - bool? refetchOnMount, - bool? refetchOnReconnect, - }) { - 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; - if (this.refetchOnReconnect == null && refetchOnReconnect != null) - this.refetchOnReconnect = refetchOnReconnect; - notifyListeners(); - } - - /// checks if the application is connected to internet in any mean - /// - /// It's true when any one this is connected - - /// - ethernet - /// - mobile - /// - wifi - Future isInternetConnected() async { - return isConnectedToInternet(await _connectivity.checkConnectivity()); - } - - /// invalidates the query - /// - /// Forcefully makes the query stale & expired which results in a refetch - /// when met conditions - 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 [DateTime.now()] is after [update_at + stale_time] it means - // the data has become stale - return DateTime.now().isAfter(updatedAt.add(_staleTime)); - } - - bool get isError => status == QueryStatus.error; - bool get isIdle => status == QueryStatus.idle; - bool get isLoading => status == QueryStatus.loading; - bool get isRefetching => status == QueryStatus.refetching; - bool get isSuccess => status == QueryStatus.success; - bool get isPreviousData { - return _previousData != null ? _previousData == data : false; - } - String get debugLabel => "Query($queryKey)"; @override @@ -408,14 +118,19 @@ class Query extends BaseOperation with AutoCast { } } - @override - String toString() { - return debugLabel; - } - @override bool operator ==(other) { return (other is Query && other.queryKey == queryKey) || identical(other, this); } + + @override + FutureOr setData() async { + data = await task(queryKey, externalData); + } + + @override + void setError(e) { + error = e; + } } diff --git a/packages/fl_query/lib/src/query_bowl.dart b/packages/fl_query/lib/src/query_bowl.dart index d6de68f..c292d91 100644 --- a/packages/fl_query/lib/src/query_bowl.dart +++ b/packages/fl_query/lib/src/query_bowl.dart @@ -437,17 +437,17 @@ class QueryBowl extends InheritedWidget { if (prevInfiniteQuery is InfiniteQuery) { // run the query if its still not called or if externalData has // changed - // if (prevQuery.prevUsedExternalData != null && - // externalData != null && - // !isShallowEqual( - // prevQuery.prevUsedExternalData!, - // externalData, - // )) { - // prevQuery.setExternalData(externalData); - // } - // prevQuery.mount(key); - // if (onData != null) prevQuery.addDataListener(onData); - // if (onError != null) prevQuery.addErrorListener(onError); + if (prevInfiniteQuery.prevUsedExternalData != null && + externalData != null && + !isShallowEqual( + prevInfiniteQuery.prevUsedExternalData!, + externalData, + )) { + prevInfiniteQuery.setExternalData(externalData); + } + prevInfiniteQuery.mount(key); + if (onData != null) prevInfiniteQuery.addDataListener(onData); + if (onError != null) prevInfiniteQuery.addErrorListener(onError); // mounting the widget that is using the query in the prevQuery return prevInfiniteQuery; } @@ -458,6 +458,7 @@ class QueryBowl extends InheritedWidget { final infiniteQuery = InfiniteQuery.fromOptions( infiniteQueryJob, externalData: externalData, + queryBowl: this, ); // if (onData != null) infiniteQuery.addDataListener(onData); // if (onError != null) infiniteQuery.addErrorListener(onError);