feat(infinite-query): add all the features of query in infinite query

Separated common logic of both `InfiniteQuery` and `Query` in a base class `BaseQuery` resulting in `InfiniteQuery` inheriting already having implemented features of `Query`
This commit is contained in:
Kingkor Roy Tirtho
2022-08-24 21:47:13 +06:00
parent 1452d7d79e
commit 61958c7d94
9 changed files with 498 additions and 411 deletions
@@ -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"}
{"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"}
@@ -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"}
{"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"}
@@ -1,7 +1,7 @@
import 'package:fl_query/src/query_bowl.dart';
import 'package:flutter/widgets.dart';
abstract class BaseOperation<Data> extends ChangeNotifier {
abstract class BaseOperation<Data, Error> 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<Data> extends ChangeNotifier {
// all properties
Data? data;
dynamic error;
Error? error;
/// total count of how many times the query retried to get a successful
/// result
+318
View File
@@ -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<T extends Object, Outside, Error>
extends BaseOperation<T, Error> 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<void> 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<T?> 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<T?> 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<void> 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<bool> 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);
}
+113 -62
View File
@@ -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<T extends Object, Outside,
PageParam extends Object>
@@ -16,84 +17,94 @@ typedef InfiniteQueryPageParamFunction<T extends Object,
= FutureOr<PageParam> Function(T lastPage, PageParam lastParam);
class InfiniteQuery<T extends Object, Outside, PageParam extends Object>
with ChangeNotifier, AutoCast {
String queryKey;
Map<PageParam, T?> _data;
Map<PageParam, dynamic> _error;
extends BaseQuery<Map<PageParam, T?>, Outside, Map<PageParam, dynamic>> {
InfiniteQueryTaskFunction<T, Outside, PageParam> task;
InfiniteQueryPageParamFunction<T, PageParam>? getNextPageParam;
InfiniteQueryPageParamFunction<T, PageParam>? getPreviousPageParam;
List<PageParam> get pageParams => _data.keys.toList();
List<dynamic> get errors => _error.values.toList();
List<T?> 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<T, Outside, PageParam> options, {
required super.queryBowl,
required Outside externalData,
}) : queryKey = options.queryKey,
task = options.task,
currentParam = options.initialParam,
_externalData = externalData,
_error = {},
QueryListener<T>? onData,
QueryListener<dynamic>? 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<void> _execute() async {
final page = await task(
queryKey,
currentParam,
_externalData,
);
_data[currentParam] = page;
notifyListeners();
}
Future<List<T?>> fetch() async {
if (_data.isEmpty) await _execute();
return pages;
}
List<PageParam> get pageParams => data?.keys.toList() ?? [];
List<dynamic> get errors => error?.values.toList() ?? [];
List<T?> get pages => data?.values.toList() ?? [];
Future<T?> fetchNextPage([
InfiniteQueryPageParamFunction<T, PageParam>? 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<T extends Object, Outside, PageParam extends Object>
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<T extends Object, Outside, PageParam extends Object>
InfiniteQueryPageParamFunction<T, PageParam>? 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<T extends Object, Outside, PageParam extends Object>
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<T, Outside, PageParam> &&
other.queryKey == queryKey) ||
identical(other, this);
}
@override
bool get hasData => data?[_currentParam] != null;
}
@@ -45,14 +45,15 @@ class _InfiniteQueryBuilderState<T extends Object, Outside,
externalData: widget.externalData,
key: uKey,
);
// final hasExternalDataChanged = infiniteQuery!.externalData != null &&
// infiniteQuery!.prevUsedExternalData != null &&
// !isShallowEqual(infiniteQuery!.externalData!, infiniteQuery!.prevUsedExternalData!);
// if (infiniteQuery!.fetched && hasExternalDataChanged) {
// await infiniteQuery!.refetch();
// } else if (!infiniteQuery!.fetched) {
await infiniteQuery!.fetch();
// }
final hasExternalDataChanged = infiniteQuery!.externalData != null &&
infiniteQuery!.prevUsedExternalData != null &&
!isShallowEqual(
infiniteQuery!.externalData!, infiniteQuery!.prevUsedExternalData!);
if (infiniteQuery!.fetched && hasExternalDataChanged) {
await infiniteQuery!.refetch();
} else if (!infiniteQuery!.fetched) {
await infiniteQuery!.fetch();
}
}
@override
+2 -1
View File
@@ -24,7 +24,8 @@ typedef MutationListener<T, V> = FutureOr<void> Function(
typedef MutationTaskFunction<T, V> = FutureOr<T> Function(
String queryKey, V variables);
class Mutation<T extends Object, V> extends BaseOperation<T> with AutoCast {
class Mutation<T extends Object, V> extends BaseOperation<T, dynamic>
with AutoCast {
// all params
final String mutationKey;
MutationTaskFunction<T, V> task;
+40 -325
View File
@@ -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<T> = FutureOr<T> Function(T? oldData);
class Query<T extends Object, Outside> extends BaseOperation<T> with AutoCast {
// all params
final String queryKey;
class Query<T extends Object, Outside> extends BaseQuery<T, Outside, dynamic> {
QueryTaskFunction<T, Outside> 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<QueryListener<T>> _onDataListeners = Set<QueryListener<T>>();
final Set<QueryListener<dynamic>> _onErrorListeners =
Set<QueryListener<dynamic>>();
// 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<T>? onData,
QueryListener<dynamic>? 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<T, Outside> options, {
@@ -115,166 +67,23 @@ class Query<T extends Object, Outside> extends BaseOperation<T> with AutoCast {
T? previousData,
QueryListener<T>? onData,
QueryListener<dynamic>? 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<void> _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<T> listener) {
_onDataListeners.add(listener);
}
void addErrorListener(QueryListener<dynamic> listener) {
_onErrorListeners.add(listener);
}
void removeDataListener(QueryListener<T> listener) {
_onDataListeners.remove(listener);
}
void removeErrorListener(QueryListener<dynamic> 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<T?> 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<T?> 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<T extends Object, Outside> extends BaseOperation<T> 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<bool> 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<T extends Object, Outside> extends BaseOperation<T> with AutoCast {
}
}
@override
String toString() {
return debugLabel;
}
@override
bool operator ==(other) {
return (other is Query<T, Outside> && other.queryKey == queryKey) ||
identical(other, this);
}
@override
FutureOr<void> setData() async {
data = await task(queryKey, externalData);
}
@override
void setError(e) {
error = e;
}
}
+12 -11
View File
@@ -437,17 +437,17 @@ class QueryBowl extends InheritedWidget {
if (prevInfiniteQuery is InfiniteQuery<T, Outside, PageParam>) {
// 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<T, Outside, PageParam>.fromOptions(
infiniteQueryJob,
externalData: externalData,
queryBowl: this,
);
// if (onData != null) infiniteQuery.addDataListener(onData);
// if (onError != null) infiniteQuery.addErrorListener(onError);