feat: clean old junk
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
final _alwaysOnlineConnectivity = AlwaysOnlineConnectivity._();
|
||||
|
||||
/// make this singleton
|
||||
class AlwaysOnlineConnectivity implements Connectivity {
|
||||
AlwaysOnlineConnectivity._();
|
||||
|
||||
factory AlwaysOnlineConnectivity() {
|
||||
return _alwaysOnlineConnectivity;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ConnectivityResult> checkConnectivity() {
|
||||
return Future.value(ConnectivityResult.ethernet);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ConnectivityResult> get onConnectivityChanged => Stream.empty();
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
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;
|
||||
final Duration retryDelay;
|
||||
|
||||
// got from global options
|
||||
@protected
|
||||
Duration cacheTime;
|
||||
|
||||
Connectivity _connectivity;
|
||||
|
||||
// all properties
|
||||
Data? data;
|
||||
Error? error;
|
||||
|
||||
/// total count of how many times the query retried to get a successful
|
||||
/// result
|
||||
int retryAttempts = 0;
|
||||
DateTime updatedAt;
|
||||
|
||||
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<ValueKey<String>> _mounts = {};
|
||||
|
||||
BaseOperation({
|
||||
required this.cacheTime,
|
||||
required this.retries,
|
||||
required this.retryDelay,
|
||||
this.data,
|
||||
Connectivity? connectivity,
|
||||
}) : updatedAt = DateTime.now(),
|
||||
_connectivity = connectivity ?? Connectivity();
|
||||
|
||||
void mount(ValueKey<String> uKey) {
|
||||
_mounts.add(uKey);
|
||||
}
|
||||
|
||||
void unmount(ValueKey<String> 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<ValueKey<String>> get mounts => _mounts;
|
||||
|
||||
/// checks if the application is connected to internet in any mean
|
||||
///
|
||||
/// It's true when any one this is connected -
|
||||
/// - ethernet
|
||||
/// - mobile
|
||||
/// - wifi
|
||||
///
|
||||
/// Deprecated: Use [isNetworkOnline] instead
|
||||
@deprecated
|
||||
Future<bool> isInternetConnected() async {
|
||||
return isNetworkOnline;
|
||||
}
|
||||
|
||||
/// 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> get isNetworkOnline =>
|
||||
_connectivity.checkConnectivity().then((v) => isConnectedToInternet(v));
|
||||
|
||||
bool get isInactive => mounts.isEmpty;
|
||||
bool get hasData => data != null;
|
||||
bool get hasError => error != null;
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query/src/base_operation.dart';
|
||||
import 'package:fl_query/src/mixins/autocast.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive/hive.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;
|
||||
|
||||
@protected
|
||||
final Set onDataListeners = Set();
|
||||
@protected
|
||||
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;
|
||||
|
||||
bool? refetchOnApplicationResume;
|
||||
bool? refetchOnWindowFocus;
|
||||
|
||||
T? _previousData;
|
||||
|
||||
BaseQuery({
|
||||
required this.queryKey,
|
||||
required Duration staleTime,
|
||||
required super.cacheTime,
|
||||
required Outside externalData,
|
||||
required super.retries,
|
||||
required super.retryDelay,
|
||||
required this.status,
|
||||
super.connectivity,
|
||||
this.refetchOnMount,
|
||||
this.refetchOnReconnect,
|
||||
this.refetchInterval,
|
||||
this.refetchOnApplicationResume,
|
||||
this.refetchOnWindowFocus,
|
||||
this.enabled = true,
|
||||
T? previousData,
|
||||
T? initialData,
|
||||
onData,
|
||||
onError,
|
||||
}) : _staleTime = staleTime,
|
||||
_initialData = initialData,
|
||||
_externalData = externalData,
|
||||
_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();
|
||||
}
|
||||
if (canCacheToDisk) {
|
||||
loadFromDisk();
|
||||
}
|
||||
}
|
||||
// all getters & setters
|
||||
|
||||
Outside get externalData => _externalData;
|
||||
Outside? get prevUsedExternalData => _prevUsedExternalData;
|
||||
|
||||
@protected
|
||||
Timer createRefetchTimer();
|
||||
|
||||
@protected
|
||||
Future<void> loadFromDisk() async {
|
||||
final box = await Hive.lazyBox<String>(kFlQueryBoxKey);
|
||||
final rawData = await box.get(queryKey);
|
||||
if (rawData == null) return;
|
||||
data = deserialize(rawData);
|
||||
if (!isLoading && !isRefetching) {
|
||||
status = QueryStatus.cached;
|
||||
}
|
||||
updatedAt = DateTime.now();
|
||||
await notifyDataListeners();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> saveToDisk({bool delete = false}) async {
|
||||
if (!canCacheToDisk) return;
|
||||
if (!hasData) {
|
||||
if (delete) {
|
||||
final box = await Hive.lazyBox<String>(kFlQueryBoxKey);
|
||||
await box.delete(queryKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final box = await Hive.lazyBox<String>(kFlQueryBoxKey);
|
||||
await box.put(queryKey, serialize(data!)!);
|
||||
}
|
||||
|
||||
/// Calls the task function & doesn't check if there's already
|
||||
/// cached data available
|
||||
@protected
|
||||
Future<void> execute() async {
|
||||
try {
|
||||
retryAttempts = 0;
|
||||
await setData();
|
||||
await saveToDisk();
|
||||
_prevUsedExternalData = _externalData;
|
||||
updatedAt = DateTime.now();
|
||||
status = QueryStatus.success;
|
||||
await notifyDataListeners();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (retries == 0) {
|
||||
status = QueryStatus.error;
|
||||
setError(e);
|
||||
await notifyErrorListeners();
|
||||
notifyListeners();
|
||||
} else {
|
||||
// retrying for retry count if failed for the first time
|
||||
while (retryAttempts <= retries) {
|
||||
await Future.delayed(retryDelay);
|
||||
try {
|
||||
await setData();
|
||||
await saveToDisk();
|
||||
_prevUsedExternalData = _externalData;
|
||||
status = QueryStatus.success;
|
||||
await notifyDataListeners();
|
||||
notifyListeners();
|
||||
break;
|
||||
} catch (e) {
|
||||
if (retryAttempts == retries) {
|
||||
status = QueryStatus.error;
|
||||
setError(e);
|
||||
await notifyErrorListeners();
|
||||
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 (isCachedData) {
|
||||
status = QueryStatus.loading;
|
||||
await execute().then((_) {
|
||||
fetched = true;
|
||||
});
|
||||
notifyListeners();
|
||||
return data;
|
||||
}
|
||||
final x = hasData && !isPreviousData;
|
||||
var isOnline = await isNetworkOnline;
|
||||
|
||||
/// if isLoading/isRefetching is true that means its already fetching/
|
||||
/// refetching. So [_execute] again can create a race condition
|
||||
if (isLoading || isRefetching || !isOnline || (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 || !(await isNetworkOnline)) 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);
|
||||
}
|
||||
|
||||
@protected
|
||||
String? serialize(T data);
|
||||
|
||||
@protected
|
||||
T? deserialize(String rawData);
|
||||
|
||||
@protected
|
||||
bool get canCacheToDisk;
|
||||
|
||||
@protected
|
||||
FutureOr<void> setData();
|
||||
@protected
|
||||
void setError(dynamic);
|
||||
@protected
|
||||
FutureOr<void> notifyDataListeners() async {
|
||||
for (final onData in onDataListeners) {
|
||||
await onData(data);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
FutureOr<void> notifyErrorListeners() async {
|
||||
for (final onError in onErrorListeners) {
|
||||
await onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
saveToDisk(delete: true);
|
||||
error = null;
|
||||
fetched = false;
|
||||
status = QueryStatus.idle;
|
||||
retryAttempts = 0;
|
||||
onDataListeners.clear();
|
||||
onErrorListeners.clear();
|
||||
mounts.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
await Hive.openLazyBox(kFlQueryBoxKey).then((box) => box.delete(queryKey));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 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,
|
||||
bool? refetchOnApplicationResume,
|
||||
bool? refetchOnWindowFocus,
|
||||
}) {
|
||||
bool updated = false;
|
||||
if (this.refetchInterval == null &&
|
||||
refetchInterval != null &&
|
||||
refetchInterval != Duration.zero) {
|
||||
this.refetchInterval = refetchInterval;
|
||||
_refetchIntervalTimer?.cancel();
|
||||
_refetchIntervalTimer = createRefetchTimer();
|
||||
updated = true;
|
||||
}
|
||||
if (this.cacheTime == Duration(minutes: 5) && cacheTime != null) {
|
||||
this.cacheTime = cacheTime;
|
||||
updated = true;
|
||||
}
|
||||
if (this._staleTime == const Duration(milliseconds: 500) &&
|
||||
staleTime != null) {
|
||||
this._staleTime = staleTime;
|
||||
updated = true;
|
||||
}
|
||||
if (this.refetchOnMount == null && refetchOnMount != null) {
|
||||
this.refetchOnMount = refetchOnMount;
|
||||
updated = true;
|
||||
}
|
||||
if (this.refetchOnReconnect == null && refetchOnReconnect != null) {
|
||||
this.refetchOnReconnect = refetchOnReconnect;
|
||||
updated = true;
|
||||
}
|
||||
if (this.refetchOnApplicationResume == null &&
|
||||
refetchOnApplicationResume != null) {
|
||||
this.refetchOnApplicationResume = refetchOnApplicationResume;
|
||||
updated = true;
|
||||
}
|
||||
if (this.refetchOnWindowFocus == null && refetchOnWindowFocus != null) {
|
||||
this.refetchOnWindowFocus = refetchOnWindowFocus;
|
||||
updated = true;
|
||||
}
|
||||
if (updated) notifyListeners();
|
||||
}
|
||||
|
||||
/// can be used to update the data manually. Can be useful when used
|
||||
/// together with mutations to perform optimistic updates or manual data
|
||||
/// updates
|
||||
/// For updating particular queries after a mutation using the
|
||||
/// `QueryBowl.refetchQueries` is more appropriate. But this one can be
|
||||
/// used when only 1 query needs get updated
|
||||
///
|
||||
/// Every time a new instance of data should be returned because of
|
||||
/// immutability
|
||||
void setQueryData(QueryUpdateFunction<T> updateFn) async {
|
||||
final newData = await updateFn(data);
|
||||
if (data == newData) return;
|
||||
data = newData;
|
||||
await saveToDisk();
|
||||
status = QueryStatus.success;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 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 isCachedData => status == QueryStatus.cached;
|
||||
bool get isPreviousData {
|
||||
return _previousData != null ? _previousData == data : false;
|
||||
}
|
||||
|
||||
String get debugLabel;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return debugLabel;
|
||||
}
|
||||
|
||||
operator ==(other);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query/src/base_query.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:queue/queue.dart';
|
||||
|
||||
typedef InfiniteQueryTaskFunction<T extends Object, Outside,
|
||||
PageParam extends Object>
|
||||
= FutureOr<T> Function(
|
||||
String queryKey,
|
||||
PageParam pageParam,
|
||||
Outside externalData,
|
||||
);
|
||||
|
||||
typedef InfiniteQueryListeners<T, PageParam extends Object> = FutureOr<void>
|
||||
Function(T page, PageParam pageParam, List<T?> pages);
|
||||
|
||||
typedef InfiniteQueryPageParamFunction<T extends Object,
|
||||
PageParam extends Object>
|
||||
= FutureOr<PageParam?> Function(T lastPage, PageParam lastParam);
|
||||
|
||||
class InfiniteQuery<T extends Object, Outside, PageParam extends Object>
|
||||
extends BaseQuery<Map<PageParam, T?>, Outside, Map<PageParam, dynamic>> {
|
||||
InfiniteQueryTaskFunction<T, Outside, PageParam> task;
|
||||
|
||||
InfiniteQueryPageParamFunction<T, PageParam>? getNextPageParam;
|
||||
InfiniteQueryPageParamFunction<T, PageParam>? getPreviousPageParam;
|
||||
|
||||
bool _hasNextPage = true;
|
||||
bool _hasPreviousPage = true;
|
||||
|
||||
bool _isFetchingNextPage = false;
|
||||
bool _isFetchingPreviousPage = false;
|
||||
|
||||
bool get isFetchingNextPage => _isFetchingNextPage;
|
||||
bool get isFetchingPreviousPage => _isFetchingPreviousPage;
|
||||
bool get hasNextPage => _hasNextPage;
|
||||
bool get hasPreviousPage => _hasPreviousPage;
|
||||
|
||||
PageParam _currentParam;
|
||||
|
||||
final Set<InfiniteQueryListeners<T, PageParam>> onDataListeners = Set();
|
||||
final Set<InfiniteQueryListeners<dynamic, PageParam>> onErrorListeners =
|
||||
Set();
|
||||
|
||||
final SerializeFunction<T>? _serializePage;
|
||||
final DeserializeFunction<T>? _deserializePage;
|
||||
|
||||
final SerializeFunction<PageParam>? serializePageParam;
|
||||
final DeserializeFunction<PageParam>? deserializePageParam;
|
||||
|
||||
InfiniteQuery({
|
||||
required super.queryKey,
|
||||
required this.task,
|
||||
required super.staleTime,
|
||||
required super.cacheTime,
|
||||
required super.externalData,
|
||||
required super.retries,
|
||||
required super.retryDelay,
|
||||
required super.status,
|
||||
required PageParam initialParam,
|
||||
super.refetchOnMount,
|
||||
super.refetchOnReconnect,
|
||||
super.refetchInterval,
|
||||
super.enabled,
|
||||
super.previousData,
|
||||
super.connectivity,
|
||||
super.refetchOnApplicationResume,
|
||||
DeserializeFunction<T>? deserialize,
|
||||
SerializeFunction<T>? serialize,
|
||||
InfiniteQueryListeners<T, PageParam>? super.onData,
|
||||
InfiniteQueryListeners<dynamic, PageParam>? super.onError,
|
||||
required T? initialPage,
|
||||
this.getNextPageParam,
|
||||
this.getPreviousPageParam,
|
||||
this.serializePageParam,
|
||||
this.deserializePageParam,
|
||||
}) : _currentParam = initialParam,
|
||||
_serializePage = serialize,
|
||||
_deserializePage = deserialize,
|
||||
super(
|
||||
initialData: {initialParam: initialPage},
|
||||
);
|
||||
|
||||
InfiniteQuery.fromOptions(
|
||||
InfiniteQueryJob<T, Outside, PageParam> options, {
|
||||
required Outside externalData,
|
||||
InfiniteQueryListeners<T, PageParam>? onData,
|
||||
InfiniteQueryListeners<dynamic, PageParam>? onError,
|
||||
}) : task = options.task,
|
||||
_currentParam = options.initialParam,
|
||||
getNextPageParam = options.getNextPageParam,
|
||||
getPreviousPageParam = options.getPreviousPageParam,
|
||||
_serializePage = options.serialize,
|
||||
_deserializePage = options.deserialize,
|
||||
serializePageParam = options.serializePageParam,
|
||||
deserializePageParam = options.deserializePageParam,
|
||||
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},
|
||||
refetchOnApplicationResume: options.refetchOnApplicationResume,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
);
|
||||
|
||||
List<PageParam> get pageParams => data?.keys.toList() ?? [];
|
||||
List<dynamic> get errors => error?.values.toList() ?? [];
|
||||
List<T?> get pages => data?.values.toList() ?? [];
|
||||
|
||||
@override
|
||||
@protected
|
||||
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 isNetworkOnline) await refetchPages();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> fetchNextPage([
|
||||
InfiniteQueryPageParamFunction<T, PageParam>? getNextPageParam,
|
||||
]) async {
|
||||
try {
|
||||
if (isFetchingNextPage ||
|
||||
isFetchingPreviousPage ||
|
||||
isLoading ||
|
||||
isRefetching) return null;
|
||||
final page = data?[_currentParam];
|
||||
if (data == null || page == null) await execute();
|
||||
_isFetchingNextPage = true;
|
||||
_isFetchingPreviousPage = false;
|
||||
final nextParam = page != null
|
||||
? await (getNextPageParam ?? this.getNextPageParam)?.call(
|
||||
page,
|
||||
_currentParam,
|
||||
)
|
||||
: null;
|
||||
if (nextParam == null) {
|
||||
_hasNextPage = false;
|
||||
notifyListeners();
|
||||
return null;
|
||||
} else {
|
||||
_hasNextPage = true;
|
||||
_currentParam = nextParam;
|
||||
return await fetch().then((_) => data?[_currentParam]);
|
||||
}
|
||||
} finally {
|
||||
_isFetchingNextPage = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> fetchPreviousPage([
|
||||
InfiniteQueryPageParamFunction<T, PageParam>? getPreviousPageParam,
|
||||
]) async {
|
||||
try {
|
||||
if (isFetchingNextPage ||
|
||||
isFetchingPreviousPage ||
|
||||
isLoading ||
|
||||
isRefetching) return null;
|
||||
_isFetchingPreviousPage = true;
|
||||
_isFetchingNextPage = false;
|
||||
notifyListeners();
|
||||
final page = data?[_currentParam];
|
||||
if (page == null) await execute();
|
||||
final prevParam = page != null
|
||||
? await (getPreviousPageParam ?? this.getPreviousPageParam)?.call(
|
||||
page,
|
||||
_currentParam,
|
||||
)
|
||||
: null;
|
||||
if (prevParam == null) {
|
||||
_hasPreviousPage = false;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
_hasPreviousPage = true;
|
||||
_currentParam = prevParam;
|
||||
return await fetch().then((_) => data?[_currentParam]);
|
||||
} catch (e) {
|
||||
print("[InfiniteQuery.fetchPreviousPage]: $e");
|
||||
rethrow;
|
||||
} finally {
|
||||
_isFetchingPreviousPage = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<T>> refetchPages([
|
||||
bool Function(T? page, PageParam pageParam, List<T?> allPages)? selector,
|
||||
]) async {
|
||||
if (isFetchingNextPage ||
|
||||
isFetchingPreviousPage ||
|
||||
isLoading ||
|
||||
isRefetching) return [];
|
||||
final refetchedPages = <T>[];
|
||||
final queue = Queue();
|
||||
for (final entry in data?.entries.toList() ?? <MapEntry<PageParam, T?>>[]) {
|
||||
final page = entry.value;
|
||||
final selected = selector?.call(page, entry.key, pages) ?? true;
|
||||
if (!selected) continue;
|
||||
_currentParam = entry.key;
|
||||
queue.add<void>(
|
||||
() async {
|
||||
final s = await refetch();
|
||||
if (s?[_currentParam] != null) {
|
||||
refetchedPages.add(s![_currentParam]!);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
await queue.onComplete;
|
||||
return refetchedPages;
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugLabel => "InfiniteQuery($queryKey)";
|
||||
|
||||
@override
|
||||
void mount(ValueKey<String> uKey) {
|
||||
super.mount(uKey);
|
||||
|
||||
/// refetching on mount if it's set to true
|
||||
/// also checking if the is stale or not
|
||||
/// no need to refetch a valid query for no reason
|
||||
if (refetchOnMount == true && isStale) {
|
||||
isNetworkOnline.then((isConnected) async {
|
||||
if (isConnected) await refetchPages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setData() async {
|
||||
if (data == null) data = Map();
|
||||
data?[_currentParam] = await task(
|
||||
queryKey,
|
||||
_currentParam,
|
||||
externalData,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setError(specError) {
|
||||
if (error is! Map) error = Map();
|
||||
error?[_currentParam] = specError;
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
FutureOr<void> notifyDataListeners() async {
|
||||
for (var onData in onDataListeners) {
|
||||
if (data?[_currentParam] == null) continue;
|
||||
onData.call(data![_currentParam]!, _currentParam, pages);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
FutureOr<void> notifyErrorListeners() async {
|
||||
for (var onError in onErrorListeners) {
|
||||
onError.call(error?[_currentParam], _currentParam, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return (other is InfiniteQuery<T, Outside, PageParam> &&
|
||||
other.queryKey == queryKey) ||
|
||||
identical(other, this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasData => data?[_currentParam] != null;
|
||||
|
||||
@override
|
||||
bool get canCacheToDisk =>
|
||||
_serializePage != null &&
|
||||
_deserializePage != null &&
|
||||
serializePageParam != null &&
|
||||
deserializePageParam != null;
|
||||
|
||||
@override
|
||||
deserialize(String rawData) {
|
||||
if (deserializePageParam == null || _deserializePage == null) return null;
|
||||
return Map.from(jsonDecode(rawData)).cast<String, String>().map(
|
||||
(key, value) {
|
||||
return MapEntry(deserializePageParam!(key), _deserializePage!(value));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
serialize(data) {
|
||||
if (serializePageParam == null || _serializePage == null) return null;
|
||||
|
||||
data.removeWhere((key, value) => value == null);
|
||||
|
||||
return jsonEncode(data.map(
|
||||
(key, value) {
|
||||
return MapEntry(
|
||||
serializePageParam!(key),
|
||||
_serializePage!(value!),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'package:fl_query/src/infinite_query.dart';
|
||||
import 'package:fl_query/src/models/infinite_query_job.dart';
|
||||
import 'package:fl_query/src/query_bowl.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class InfiniteQueryBuilder<T extends Object, Outside, PageParam extends Object>
|
||||
extends StatefulWidget {
|
||||
final Function(
|
||||
BuildContext context,
|
||||
InfiniteQuery<T, Outside, PageParam> query,
|
||||
) builder;
|
||||
final InfiniteQueryJob<T, Outside, PageParam> job;
|
||||
final Outside externalData;
|
||||
final InfiniteQueryListeners<T, PageParam>? onData;
|
||||
final InfiniteQueryListeners<dynamic, PageParam>? onError;
|
||||
|
||||
InfiniteQueryBuilder({
|
||||
required this.job,
|
||||
required this.builder,
|
||||
required this.externalData,
|
||||
this.onData,
|
||||
this.onError,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<InfiniteQueryBuilder<T, Outside, PageParam>> createState() =>
|
||||
_InfiniteQueryBuilderState<T, Outside, PageParam>();
|
||||
}
|
||||
|
||||
class _InfiniteQueryBuilderState<T extends Object, Outside,
|
||||
PageParam extends Object>
|
||||
extends State<InfiniteQueryBuilder<T, Outside, PageParam>> {
|
||||
InfiniteQuery<T, Outside, PageParam>? infiniteQuery;
|
||||
late QueryBowl queryBowl;
|
||||
late final ValueKey<String> uKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
uKey = ValueKey<String>(uuid.v4());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
init();
|
||||
QueryBowl.of(context).onInfiniteQueriesUpdate<T, Outside, PageParam>(
|
||||
(infiniteQuery) async {
|
||||
if (infiniteQuery.queryKey != widget.job.queryKey) return;
|
||||
if (mounted)
|
||||
setState(() {
|
||||
this.infiniteQuery = infiniteQuery;
|
||||
});
|
||||
if (infiniteQuery.isCachedData && !infiniteQuery.fetched)
|
||||
await infiniteQuery.refetchPages();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void init([T? previousData]) async {
|
||||
final bowl = QueryBowl.of(context);
|
||||
final prevInfiniteQuery =
|
||||
bowl.getInfiniteQuery<T, Outside, PageParam>(widget.job.queryKey);
|
||||
infiniteQuery = bowl.addInfiniteQuery<T, Outside, PageParam>(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
key: uKey,
|
||||
);
|
||||
final hasExternalDataChanged = prevInfiniteQuery != null
|
||||
? !isShallowEqual(
|
||||
prevInfiniteQuery.externalData,
|
||||
prevInfiniteQuery.prevUsedExternalData,
|
||||
)
|
||||
: !isShallowEqual(
|
||||
infiniteQuery!.externalData,
|
||||
infiniteQuery!.prevUsedExternalData,
|
||||
);
|
||||
|
||||
if (infiniteQuery!.fetched && hasExternalDataChanged) {
|
||||
await infiniteQuery!.refetchPages();
|
||||
} else if (!infiniteQuery!.fetched) {
|
||||
await infiniteQuery!.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
final hasOnErrorChanged =
|
||||
oldWidget.onError != widget.onError && oldWidget.onError != null;
|
||||
final hasOnDataChanged =
|
||||
oldWidget.onData != widget.onData && oldWidget.onData != null;
|
||||
|
||||
// re-init the query-builder when new queryJob is appended
|
||||
if (oldWidget.job.queryKey != widget.job.queryKey) {
|
||||
_infiniteQueryDispose();
|
||||
init();
|
||||
|
||||
/// setting the new query's initial data as prev query's data
|
||||
/// when [job.keepPreviousData] is true and both are dynamic
|
||||
// if (oldWidget.job.isDynamic &&
|
||||
// widget.job.isDynamic &&
|
||||
// oldWidget.job.keepPreviousData == true &&
|
||||
// widget.job.keepPreviousData == true) {
|
||||
// init(infiniteQuery?.pages);
|
||||
// } else {
|
||||
// init();
|
||||
// }
|
||||
} else if (!isShallowEqual(oldWidget.externalData, widget.externalData)) {
|
||||
if (widget.job.refetchOnExternalDataChange ??
|
||||
queryBowl.refetchOnExternalDataChange) {
|
||||
QueryBowl.of(context).addInfiniteQuery(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
key: uKey,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
)..refetchPages();
|
||||
} else {
|
||||
QueryBowl.of(context)
|
||||
.getQuery(widget.job.queryKey)
|
||||
?.setExternalData(widget.externalData);
|
||||
}
|
||||
if (hasOnDataChanged)
|
||||
infiniteQuery?.removeDataListener(oldWidget.onData!);
|
||||
if (hasOnErrorChanged)
|
||||
infiniteQuery?.removeErrorListener(oldWidget.onError!);
|
||||
} else {
|
||||
if (hasOnDataChanged) {
|
||||
infiniteQuery?.removeDataListener(oldWidget.onData!);
|
||||
if (widget.onData != null)
|
||||
infiniteQuery?.addDataListener(widget.onData!);
|
||||
}
|
||||
if (hasOnErrorChanged) {
|
||||
infiniteQuery?.removeErrorListener(oldWidget.onError!);
|
||||
if (widget.onError != null)
|
||||
infiniteQuery?.addErrorListener(widget.onError!);
|
||||
}
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
_infiniteQueryDispose() {
|
||||
infiniteQuery?.unmount(uKey);
|
||||
if (widget.onData != null)
|
||||
infiniteQuery?.removeDataListener(widget.onData!);
|
||||
if (widget.onError != null)
|
||||
infiniteQuery?.removeErrorListener(widget.onError!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
queryBowl = QueryBowl.of(context);
|
||||
final latestInfiniteQuery = queryBowl
|
||||
.getInfiniteQuery<T, Outside, PageParam>(widget.job.queryKey) ??
|
||||
infiniteQuery;
|
||||
if (latestInfiniteQuery == null) return Container();
|
||||
return widget.builder(context, latestInfiniteQuery);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mixin AutoCast {
|
||||
A? cast<A>() => this is A ? this as A : null;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
class InfiniteQueryData<T extends Object> {
|
||||
final Set<T> pages;
|
||||
final Set<String> pageParams;
|
||||
|
||||
InfiniteQueryData({
|
||||
required this.pages,
|
||||
required this.pageParams,
|
||||
});
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/src/infinite_query.dart';
|
||||
import 'package:fl_query/src/models/query_job.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class InfiniteQueryJob<T extends Object, Outside, PageParam extends Object> {
|
||||
// all params
|
||||
String _queryKey;
|
||||
InfiniteQueryTaskFunction<T, Outside, PageParam> task;
|
||||
SerializeFunction<T>? serialize;
|
||||
DeserializeFunction<T>? deserialize;
|
||||
SerializeFunction<PageParam>? serializePageParam;
|
||||
DeserializeFunction<PageParam>? deserializePageParam;
|
||||
final int? retries;
|
||||
final Duration? retryDelay;
|
||||
T? initialPage;
|
||||
PageParam initialParam;
|
||||
|
||||
/// If set to false then the initial fetch will not be called & to
|
||||
/// start the process the user has to call the refetch first
|
||||
final bool? enabled;
|
||||
|
||||
// got from global options
|
||||
final bool? refetchOnMount;
|
||||
final bool? refetchOnReconnect;
|
||||
final bool? refetchOnExternalDataChange;
|
||||
final bool? refetchOnApplicationResume;
|
||||
final bool? refetchOnWindowFocus;
|
||||
final Duration? staleTime;
|
||||
final Duration? cacheTime;
|
||||
|
||||
final Duration? refetchInterval;
|
||||
final Connectivity? connectivity;
|
||||
final InfiniteQueryPageParamFunction<T, PageParam> getNextPageParam;
|
||||
final InfiniteQueryPageParamFunction<T, PageParam> getPreviousPageParam;
|
||||
|
||||
@protected
|
||||
bool isDynamic = false;
|
||||
|
||||
InfiniteQueryJob({
|
||||
required String queryKey,
|
||||
required this.task,
|
||||
required this.initialParam,
|
||||
required this.getNextPageParam,
|
||||
required this.getPreviousPageParam,
|
||||
this.retries,
|
||||
this.retryDelay,
|
||||
this.initialPage,
|
||||
this.staleTime,
|
||||
this.cacheTime,
|
||||
this.enabled,
|
||||
this.refetchInterval,
|
||||
this.refetchOnMount,
|
||||
this.refetchOnReconnect,
|
||||
this.refetchOnExternalDataChange,
|
||||
this.refetchOnApplicationResume,
|
||||
this.refetchOnWindowFocus,
|
||||
this.connectivity,
|
||||
this.deserialize,
|
||||
this.serialize,
|
||||
this.serializePageParam,
|
||||
this.deserializePageParam,
|
||||
}) : assert(
|
||||
serialize == null &&
|
||||
deserialize == null &&
|
||||
serializePageParam == null &&
|
||||
deserializePageParam == null ||
|
||||
(serialize != null &&
|
||||
deserialize != null &&
|
||||
serializePageParam != null &&
|
||||
deserializePageParam != null &&
|
||||
enabled != false),
|
||||
"All or none of the serialize, deserialize, serializePageParam & deserializePageParam function must be provided. And `enabled` must be true if all of them are provided.",
|
||||
),
|
||||
_queryKey = queryKey;
|
||||
|
||||
String get queryKey => _queryKey;
|
||||
|
||||
static InfiniteQueryJob<T, Outside, PageParam> Function(String queryKey)
|
||||
withVariableKey<T extends Object, Outside, PageParam extends Object>({
|
||||
required InfiniteQueryTaskFunction<T, Outside, PageParam> task,
|
||||
required InfiniteQueryPageParamFunction<T, PageParam> getNextPageParam,
|
||||
required InfiniteQueryPageParamFunction<T, PageParam> getPreviousPageParam,
|
||||
required PageParam initialParam,
|
||||
|
||||
/// a extra key joined with queryKey by a '#'
|
||||
///
|
||||
/// useful for matching a group query
|
||||
String? preQueryKey,
|
||||
int? retries,
|
||||
Duration? retryDelay,
|
||||
T? initialPage,
|
||||
Duration? staleTime,
|
||||
Duration? cacheTime,
|
||||
bool? enabled,
|
||||
Duration? refetchInterval,
|
||||
bool? refetchOnMount,
|
||||
bool? refetchOnReconnect,
|
||||
bool? refetchOnExternalDataChange,
|
||||
bool? refetchOnApplicationResume,
|
||||
bool? refetchOnWindowFocus,
|
||||
Connectivity? connectivity,
|
||||
SerializeFunction<T>? serialize,
|
||||
DeserializeFunction<T>? deserialize,
|
||||
SerializeFunction<PageParam>? serializePageParam,
|
||||
DeserializeFunction<PageParam>? deserializePageParam,
|
||||
}) {
|
||||
return (String queryKey) {
|
||||
if (preQueryKey != null) queryKey = "$preQueryKey#$queryKey";
|
||||
final query = InfiniteQueryJob<T, Outside, PageParam>(
|
||||
queryKey: queryKey,
|
||||
task: task,
|
||||
getNextPageParam: getNextPageParam,
|
||||
getPreviousPageParam: getPreviousPageParam,
|
||||
retries: retries,
|
||||
retryDelay: retryDelay,
|
||||
initialPage: initialPage,
|
||||
staleTime: staleTime,
|
||||
cacheTime: cacheTime,
|
||||
enabled: enabled,
|
||||
refetchInterval: refetchInterval,
|
||||
refetchOnMount: refetchOnMount,
|
||||
refetchOnReconnect: refetchOnReconnect,
|
||||
refetchOnExternalDataChange: refetchOnExternalDataChange,
|
||||
refetchOnApplicationResume: refetchOnApplicationResume,
|
||||
refetchOnWindowFocus: refetchOnWindowFocus,
|
||||
connectivity: connectivity,
|
||||
initialParam: initialParam,
|
||||
serialize: serialize,
|
||||
deserialize: deserialize,
|
||||
serializePageParam: serializePageParam,
|
||||
deserializePageParam: deserializePageParam,
|
||||
);
|
||||
query.isDynamic = true;
|
||||
return query;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
|
||||
class MutationJob<T extends Object, V> {
|
||||
String _mutationKey;
|
||||
MutationTaskFunction<T, V> task;
|
||||
final int? retries;
|
||||
final Duration? retryDelay;
|
||||
final Duration? cacheTime;
|
||||
final Connectivity? connectivity;
|
||||
|
||||
MutationJob({
|
||||
required String mutationKey,
|
||||
required this.task,
|
||||
this.connectivity,
|
||||
this.retries,
|
||||
this.retryDelay,
|
||||
this.cacheTime,
|
||||
}) : _mutationKey = mutationKey;
|
||||
|
||||
String get mutationKey => _mutationKey;
|
||||
|
||||
static MutationJob<T, V> Function(String queryKey)
|
||||
withVariableKey<T extends Object, V>({
|
||||
required MutationTaskFunction<T, V> task,
|
||||
|
||||
/// a extra key joined with mutationKey by a '#'
|
||||
///
|
||||
/// useful for matching a group mutation
|
||||
String? preMutationKey,
|
||||
int? retries,
|
||||
Duration? retryDelay,
|
||||
Duration? cacheTime,
|
||||
Connectivity? connectivity,
|
||||
}) {
|
||||
return (String mutationKey) {
|
||||
if (preMutationKey != null) mutationKey = "$preMutationKey#$mutationKey";
|
||||
return MutationJob<T, V>(
|
||||
mutationKey: mutationKey,
|
||||
task: task,
|
||||
retries: retries,
|
||||
retryDelay: retryDelay,
|
||||
cacheTime: cacheTime,
|
||||
connectivity: connectivity,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/src/query.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef SerializeFunction<T extends Object> = String Function(T data);
|
||||
typedef DeserializeFunction<T extends Object> = T Function(String raw);
|
||||
|
||||
class QueryJob<T extends Object, Outside> {
|
||||
// all params
|
||||
String _queryKey;
|
||||
QueryTaskFunction<T, Outside> task;
|
||||
final SerializeFunction<T>? serialize;
|
||||
final DeserializeFunction<T>? deserialize;
|
||||
final int? retries;
|
||||
final Duration? retryDelay;
|
||||
T? initialData;
|
||||
|
||||
/// If set to false then the initial fetch will not be called & to
|
||||
/// start the process the user has to call the refetch first
|
||||
final bool? enabled;
|
||||
|
||||
// got from global options
|
||||
final bool? refetchOnMount;
|
||||
final bool? refetchOnReconnect;
|
||||
final bool? refetchOnExternalDataChange;
|
||||
final bool? refetchOnApplicationResume;
|
||||
final bool? refetchOnWindowFocus;
|
||||
final bool? keepPreviousData;
|
||||
final Duration? staleTime;
|
||||
final Duration? cacheTime;
|
||||
|
||||
final Duration? refetchInterval;
|
||||
final Connectivity? connectivity;
|
||||
|
||||
@protected
|
||||
bool isDynamic = false;
|
||||
|
||||
QueryJob({
|
||||
required String queryKey,
|
||||
required this.task,
|
||||
this.retries,
|
||||
this.retryDelay,
|
||||
this.initialData,
|
||||
this.staleTime,
|
||||
this.cacheTime,
|
||||
this.enabled,
|
||||
this.refetchInterval,
|
||||
this.refetchOnMount,
|
||||
this.refetchOnReconnect,
|
||||
this.refetchOnExternalDataChange,
|
||||
this.connectivity,
|
||||
this.keepPreviousData,
|
||||
this.refetchOnApplicationResume,
|
||||
this.refetchOnWindowFocus,
|
||||
this.deserialize,
|
||||
this.serialize,
|
||||
}) : assert(
|
||||
serialize == null && deserialize == null ||
|
||||
(serialize != null && deserialize != null && enabled != false),
|
||||
"Both or none of the serialize and deserialize function must be provided and enabled must be true if you want to use disk caching",
|
||||
),
|
||||
_queryKey = queryKey;
|
||||
|
||||
String get queryKey => _queryKey;
|
||||
|
||||
static QueryJob<T, Outside> Function(String queryKey)
|
||||
withVariableKey<T extends Object, Outside>({
|
||||
required QueryTaskFunction<T, Outside> 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,
|
||||
bool? refetchOnReconnect,
|
||||
bool? refetchOnExternalDataChange,
|
||||
bool? refetchOnApplicationResume,
|
||||
bool? refetchOnWindowFocus,
|
||||
Connectivity? connectivity,
|
||||
bool? keepPreviousData,
|
||||
SerializeFunction<T>? serialize,
|
||||
DeserializeFunction<T>? deserialize,
|
||||
}) {
|
||||
return (String queryKey) {
|
||||
if (preQueryKey != null) queryKey = "$preQueryKey#$queryKey";
|
||||
final query = QueryJob<T, Outside>(
|
||||
queryKey: queryKey,
|
||||
task: task,
|
||||
retries: retries,
|
||||
retryDelay: retryDelay,
|
||||
initialData: initialData,
|
||||
staleTime: staleTime,
|
||||
cacheTime: cacheTime,
|
||||
enabled: enabled,
|
||||
refetchInterval: refetchInterval,
|
||||
refetchOnMount: refetchOnMount,
|
||||
refetchOnReconnect: refetchOnReconnect,
|
||||
refetchOnExternalDataChange: refetchOnExternalDataChange,
|
||||
connectivity: connectivity,
|
||||
keepPreviousData: keepPreviousData,
|
||||
refetchOnApplicationResume: refetchOnApplicationResume,
|
||||
refetchOnWindowFocus: refetchOnWindowFocus,
|
||||
serialize: serialize,
|
||||
deserialize: deserialize,
|
||||
);
|
||||
query.isDynamic = true;
|
||||
return query;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query/src/base_operation.dart';
|
||||
import 'package:fl_query/src/mixins/autocast.dart';
|
||||
import 'package:fl_query/src/models/mutation_job.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum MutationStatus {
|
||||
error,
|
||||
success,
|
||||
loading,
|
||||
idle,
|
||||
}
|
||||
|
||||
typedef MutationListenerReturnable<T, R> = FutureOr<R> Function(T);
|
||||
|
||||
typedef MutationListener<T, V> = FutureOr<void> Function(
|
||||
T payload,
|
||||
V variables,
|
||||
dynamic realData,
|
||||
);
|
||||
|
||||
typedef MutationTaskFunction<T, V> = FutureOr<T> Function(
|
||||
String queryKey, V variables);
|
||||
|
||||
class Mutation<T extends Object, V> extends BaseOperation<T, dynamic>
|
||||
with AutoCast {
|
||||
// all params
|
||||
final String mutationKey;
|
||||
MutationTaskFunction<T, V> task;
|
||||
|
||||
MutationStatus status;
|
||||
|
||||
dynamic _sideEffectContext;
|
||||
|
||||
@protected
|
||||
final Set<MutationListener<T, V>> _onDataListeners = {};
|
||||
@protected
|
||||
final Set<MutationListener<dynamic, V>> _onErrorListeners = {};
|
||||
@protected
|
||||
final Set<MutationListenerReturnable<V, dynamic>> _onMutateListeners = {};
|
||||
|
||||
// using late as _variables will only be used after a [mutate] or
|
||||
// [mutateAsync] is executed
|
||||
late V _variables;
|
||||
|
||||
Mutation({
|
||||
required this.mutationKey,
|
||||
required this.task,
|
||||
required super.retries,
|
||||
required super.retryDelay,
|
||||
super.connectivity,
|
||||
required Duration cacheTime,
|
||||
MutationListener<T, V>? onData,
|
||||
MutationListener<dynamic, V>? onError,
|
||||
MutationListenerReturnable<V, dynamic>? onMutate,
|
||||
}) : status = MutationStatus.idle,
|
||||
super(cacheTime: cacheTime) {
|
||||
if (onData != null) _onDataListeners.add(onData);
|
||||
if (onError != null) _onErrorListeners.add(onError);
|
||||
if (onMutate != null) _onMutateListeners.add(onMutate);
|
||||
}
|
||||
|
||||
Mutation.fromOptions(
|
||||
MutationJob<T, V> options, {
|
||||
MutationListener<T, V>? onData,
|
||||
MutationListener<dynamic, V>? onError,
|
||||
MutationListenerReturnable<V, dynamic>? onMutate,
|
||||
}) : mutationKey = options.mutationKey,
|
||||
task = options.task,
|
||||
status = MutationStatus.idle,
|
||||
super(
|
||||
retries: options.retries ?? 3,
|
||||
retryDelay: options.retryDelay ?? const Duration(milliseconds: 200),
|
||||
cacheTime: options.cacheTime ?? const Duration(minutes: 5),
|
||||
connectivity: options.connectivity,
|
||||
) {
|
||||
if (onData != null) _onDataListeners.add(onData);
|
||||
if (onError != null) _onErrorListeners.add(onError);
|
||||
}
|
||||
|
||||
// all methods
|
||||
|
||||
/// Calls the task function & doesn't check if there's already
|
||||
/// cached data available
|
||||
Future<void> _execute(V variables) async {
|
||||
try {
|
||||
status = MutationStatus.loading;
|
||||
notifyListeners();
|
||||
retryAttempts = 0;
|
||||
for (final onMutate in _onMutateListeners) {
|
||||
_sideEffectContext = await onMutate(variables);
|
||||
}
|
||||
data = await task(mutationKey, variables);
|
||||
updatedAt = DateTime.now();
|
||||
status = MutationStatus.success;
|
||||
for (final onData in _onDataListeners) {
|
||||
onData(data!, _variables, _sideEffectContext);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
if (retries == 0) {
|
||||
status = MutationStatus.error;
|
||||
error = e;
|
||||
for (final onError in _onErrorListeners) {
|
||||
onError(error, variables, _sideEffectContext);
|
||||
}
|
||||
notifyListeners();
|
||||
throw e;
|
||||
} else {
|
||||
// retrying for retry count if failed for the first time
|
||||
while (retryAttempts <= retries) {
|
||||
await Future.delayed(retryDelay);
|
||||
try {
|
||||
for (final onMutate in _onMutateListeners) {
|
||||
_sideEffectContext = onMutate(variables);
|
||||
}
|
||||
data = await task(mutationKey, variables);
|
||||
status = MutationStatus.success;
|
||||
for (final onData in _onDataListeners) {
|
||||
onData(data!, variables, _sideEffectContext);
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
} catch (e) {
|
||||
if (retryAttempts == retries) {
|
||||
status = MutationStatus.error;
|
||||
error = e;
|
||||
for (final onError in _onErrorListeners) {
|
||||
onError(error, variables, _sideEffectContext);
|
||||
}
|
||||
notifyListeners();
|
||||
throw e;
|
||||
}
|
||||
retryAttempts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addDataListener(MutationListener<T, V> listener) {
|
||||
_onDataListeners.add(listener);
|
||||
}
|
||||
|
||||
void addErrorListener(MutationListener<dynamic, V> listener) {
|
||||
_onErrorListeners.add(listener);
|
||||
}
|
||||
|
||||
void addMutateListener(MutationListenerReturnable<V, dynamic> listener) {
|
||||
_onMutateListeners.add(listener);
|
||||
}
|
||||
|
||||
void removeDataListener(MutationListener<T, V> listener) {
|
||||
_onDataListeners.remove(listener);
|
||||
}
|
||||
|
||||
void removeErrorListener(MutationListener<dynamic, V> listener) {
|
||||
_onErrorListeners.remove(listener);
|
||||
}
|
||||
|
||||
void removeMutateListener(MutationListenerReturnable<V, dynamic> listener) {
|
||||
_onMutateListeners.remove(listener);
|
||||
}
|
||||
|
||||
void mutate(
|
||||
V variables, {
|
||||
MutationListener<T, V>? onData,
|
||||
MutationListener<dynamic, V>? onError,
|
||||
}) {
|
||||
_variables = variables;
|
||||
if (onData != null) _onDataListeners.add(onData);
|
||||
if (onError != null) _onErrorListeners.add(onError);
|
||||
try {
|
||||
_execute(variables).then((_) {
|
||||
_onDataListeners.remove(onData);
|
||||
_onErrorListeners.remove(onError);
|
||||
});
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> mutateAsync(V variables) async {
|
||||
_variables = variables;
|
||||
return await _execute(variables).then((_) => data);
|
||||
}
|
||||
|
||||
/// 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.idle;
|
||||
_onMutateListeners.clear();
|
||||
_sideEffectContext = null;
|
||||
}
|
||||
|
||||
bool get isError => status == MutationStatus.error;
|
||||
bool get isIdle => status == MutationStatus.idle;
|
||||
bool get isLoading => status == MutationStatus.loading;
|
||||
bool get isSuccess => status == MutationStatus.success;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return (other is Mutation<T, V> && other.mutationKey == mutationKey) ||
|
||||
identical(other, this);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'package:fl_query/src/models/mutation_job.dart';
|
||||
import 'package:fl_query/src/mutation.dart';
|
||||
import 'package:fl_query/src/query_bowl.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class MutationBuilder<T extends Object, V> extends StatefulWidget {
|
||||
final Function(BuildContext context, Mutation<T, V> mutation) builder;
|
||||
final MutationJob<T, V> job;
|
||||
|
||||
/// Called when the query returns new data, on query
|
||||
/// refetch or query gets expired
|
||||
final MutationListener<T, V>? onData;
|
||||
|
||||
/// Called when the query returns error
|
||||
final MutationListener<dynamic, V>? onError;
|
||||
|
||||
/// called right before the mutation is about to run
|
||||
///
|
||||
/// perfect scenario for doing optimistic updates
|
||||
final MutationListenerReturnable<V, dynamic>? onMutate;
|
||||
|
||||
const MutationBuilder({
|
||||
required this.job,
|
||||
required this.builder,
|
||||
this.onData,
|
||||
this.onError,
|
||||
this.onMutate,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MutationBuilder<T, V>> createState() => _MutationBuilderState<T, V>();
|
||||
}
|
||||
|
||||
class _MutationBuilderState<T extends Object, V>
|
||||
extends State<MutationBuilder<T, V>> {
|
||||
late ValueKey<String> uKey;
|
||||
|
||||
Mutation<T, V>? mutation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
uKey = ValueKey<String>(uuid.v4());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
init();
|
||||
QueryBowl.of(context).onMutationsUpdate<T, V>(
|
||||
(mutation) {
|
||||
if (mutation.mutationKey != widget.job.mutationKey || !mounted)
|
||||
return;
|
||||
setState(() {
|
||||
this.mutation = mutation;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void init([_]) {
|
||||
final bowl = QueryBowl.of(context);
|
||||
|
||||
setState(() {
|
||||
mutation = bowl.addMutation<T, V>(
|
||||
widget.job,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
onMutate: widget.onMutate,
|
||||
key: uKey,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MutationBuilder<T, V> oldWidget) {
|
||||
if (oldWidget.job.mutationKey != widget.job.mutationKey) {
|
||||
_mutationDispose();
|
||||
init();
|
||||
} else {
|
||||
if (oldWidget.onData != widget.onData && oldWidget.onData != null) {
|
||||
mutation?.removeDataListener(oldWidget.onData!);
|
||||
if (widget.onData != null) mutation?.addDataListener(widget.onData!);
|
||||
}
|
||||
if (oldWidget.onError != widget.onError && oldWidget.onError != null) {
|
||||
mutation?.removeErrorListener(oldWidget.onError!);
|
||||
if (widget.onError != null) mutation?.addErrorListener(widget.onError!);
|
||||
}
|
||||
if (oldWidget.onMutate != widget.onMutate && oldWidget.onMutate != null) {
|
||||
mutation?.removeMutateListener(oldWidget.onMutate!);
|
||||
if (widget.onMutate != null)
|
||||
mutation?.addMutateListener(widget.onMutate!);
|
||||
}
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mutationDispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _mutationDispose() {
|
||||
mutation?.unmount(uKey);
|
||||
if (widget.onData != null) mutation?.addDataListener(widget.onData!);
|
||||
if (widget.onError != null) mutation?.addErrorListener(widget.onError!);
|
||||
if (widget.onMutate != null) mutation?.addMutateListener(widget.onMutate!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (mutation == null) return Container();
|
||||
return widget.builder(context, mutation!);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.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:flutter/widgets.dart';
|
||||
|
||||
enum QueryStatus {
|
||||
/// 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,
|
||||
|
||||
/// when the query data is loaded from cache
|
||||
cached,
|
||||
}
|
||||
|
||||
typedef QueryTaskFunction<T extends Object, Outside> = FutureOr<T> Function(
|
||||
String queryKey,
|
||||
Outside externalData,
|
||||
);
|
||||
|
||||
typedef QueryListener<T> = FutureOr<void> Function(T);
|
||||
|
||||
typedef ListenerUnsubscriber = void Function();
|
||||
|
||||
typedef QueryUpdateFunction<T> = FutureOr<T> Function(T? oldData);
|
||||
|
||||
class Query<T extends Object, Outside> extends BaseQuery<T, Outside, dynamic> {
|
||||
QueryTaskFunction<T, Outside> task;
|
||||
|
||||
final Set<QueryListener<T>> onDataListeners = Set();
|
||||
final Set<QueryListener<dynamic>> onErrorListeners = Set();
|
||||
|
||||
final SerializeFunction<T>? _serialize;
|
||||
final DeserializeFunction<T>? _deserialize;
|
||||
|
||||
Query({
|
||||
required super.queryKey,
|
||||
required this.task,
|
||||
required super.staleTime,
|
||||
required super.cacheTime,
|
||||
required super.externalData,
|
||||
required super.retries,
|
||||
required super.retryDelay,
|
||||
required super.status,
|
||||
super.refetchOnMount,
|
||||
super.refetchOnReconnect,
|
||||
super.refetchInterval,
|
||||
super.enabled,
|
||||
super.previousData,
|
||||
super.connectivity,
|
||||
super.initialData,
|
||||
super.refetchOnApplicationResume,
|
||||
SerializeFunction<T>? serialize,
|
||||
DeserializeFunction<T>? deserialize,
|
||||
QueryListener<T>? super.onData,
|
||||
QueryListener<dynamic>? super.onError,
|
||||
}) : _deserialize = deserialize,
|
||||
_serialize = serialize;
|
||||
|
||||
Query.fromOptions(
|
||||
QueryJob<T, Outside> options, {
|
||||
required Outside externalData,
|
||||
T? previousData,
|
||||
QueryListener<T>? onData,
|
||||
QueryListener<dynamic>? onError,
|
||||
}) : task = options.task,
|
||||
_deserialize = options.deserialize,
|
||||
_serialize = options.serialize,
|
||||
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),
|
||||
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,
|
||||
refetchOnApplicationResume: options.refetchOnApplicationResume,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
);
|
||||
|
||||
@override
|
||||
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 isNetworkOnline) await refetch();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String get debugLabel => "Query($queryKey)";
|
||||
|
||||
@override
|
||||
void mount(ValueKey<String> uKey) {
|
||||
super.mount(uKey);
|
||||
|
||||
/// refetching on mount if it's set to true
|
||||
/// also checking if the is stale or not
|
||||
/// no need to refetch a valid query for no reason
|
||||
if (refetchOnMount == true && isStale) {
|
||||
isNetworkOnline.then((isConnected) async {
|
||||
if (isConnected) await refetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@override
|
||||
deserialize(rawData) {
|
||||
if (_deserialize == null) return null;
|
||||
return _deserialize!(rawData);
|
||||
}
|
||||
|
||||
@override
|
||||
serialize(data) {
|
||||
if (_serialize == null) return null;
|
||||
return _serialize!(data);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get canCacheToDisk => _serialize != null && _deserialize != null;
|
||||
}
|
||||
@@ -1,565 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query/src/query_cache.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
/// The widget that holds every [Query] & [Mutation] to your
|
||||
/// entire Flutter application in anywhere
|
||||
/// [QueryBowl] provides an imperative way to handle all the query &
|
||||
/// mutation related methods & properties.
|
||||
/// Its responsible or can be used for (not recommended) creating,
|
||||
/// updating & deleting queries & mutations
|
||||
///
|
||||
/// This must be used along with [QueryBowlScope]
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return QueryBowlScope(
|
||||
/// bowl: QueryBowl(),
|
||||
/// child: MaterialApp(/*...other stuff...*/),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
class QueryBowl {
|
||||
/// global stale time
|
||||
///
|
||||
/// Makes [Query.data] stale after crossing the duration of provided
|
||||
/// [staleTime]
|
||||
final Duration staleTime;
|
||||
|
||||
// refetching options
|
||||
|
||||
/// refetch query when new query instance mounts
|
||||
final bool refetchOnMount;
|
||||
|
||||
/// refetch when desktop/web app regains Focus
|
||||
final bool refetchOnWindowFocus;
|
||||
|
||||
/// the delay to call each query when desktop/web application gets focused after
|
||||
/// being unfocused. Using a delay after each refetch so refetching all the
|
||||
/// queries at once won't create high CPU spikes & also wouldn't violate
|
||||
/// rate-limit
|
||||
///
|
||||
/// Though its recommended most of the time to use but it can be turned
|
||||
/// off by passing [Duration.zero]
|
||||
final Duration refetchOnWindowFocusDelay;
|
||||
|
||||
/// refetch when application resumes from the background in mobile devices
|
||||
final bool refetchOnApplicationResume;
|
||||
|
||||
/// the delay to call each query when app resumes from the background in
|
||||
/// mobile devices. Using a delay after each refetch so refetching all the
|
||||
/// queries at once won't create high CPU spikes & also wouldn't violate
|
||||
/// rate-limit
|
||||
///
|
||||
/// Though its recommended most of the time to use but it can be turned
|
||||
/// off by passing [Duration.zero]
|
||||
final Duration refetchOnApplicationResumeDelay;
|
||||
|
||||
/// refetch when user's device reconnects to the internet after not being
|
||||
/// connected before
|
||||
final bool refetchOnReconnect;
|
||||
|
||||
/// the delay to call each query when user device reconnects to the
|
||||
/// internet. Using a delay after each refetch so refetching all the
|
||||
/// queries at once won't create high CPU spikes & also wouldn't violate
|
||||
/// rate-limit
|
||||
///
|
||||
/// Though its recommended most of the time to use but it can be turned
|
||||
/// off by passing [Duration.zero]
|
||||
final Duration refetchOnReconnectDelay;
|
||||
|
||||
/// Refetch the query whenever the data passed as [externalData] to any
|
||||
/// [QueryBuilder] or [useQuery] changes
|
||||
///
|
||||
/// If set to false than the [externalData] will get updated but there
|
||||
/// won't be any query update
|
||||
final bool refetchOnExternalDataChange;
|
||||
|
||||
/// used for periodically checking if any query got stale.
|
||||
/// If none is supplied then half of the value of staleTime is used
|
||||
final Duration refetchInterval;
|
||||
|
||||
/// The Cache that holds all the queries, mutations and infinite queries
|
||||
QueryCache cache;
|
||||
|
||||
QueryBowl({
|
||||
/// The Cache that holds all the queries, mutations and infinite
|
||||
/// queries
|
||||
QueryCache? cache,
|
||||
this.staleTime = Duration.zero,
|
||||
|
||||
/// global cache time
|
||||
///
|
||||
/// Removes inactive queries after provided duration of [cacheTime]
|
||||
Duration? cacheTime,
|
||||
this.refetchInterval = Duration.zero,
|
||||
this.refetchOnMount = false,
|
||||
this.refetchOnReconnect = true,
|
||||
this.refetchOnReconnectDelay = const Duration(milliseconds: 100),
|
||||
this.refetchOnApplicationResumeDelay = const Duration(milliseconds: 100),
|
||||
this.refetchOnWindowFocusDelay = const Duration(milliseconds: 100),
|
||||
this.refetchOnApplicationResume = true,
|
||||
this.refetchOnWindowFocus = true,
|
||||
this.refetchOnExternalDataChange = false,
|
||||
}) : cache = cache ?? QueryCache(cacheTime: cacheTime) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((ConnectivityResult result) async {
|
||||
if (isConnectedToInternet(result)) {
|
||||
for (final query in this.cache.queries.toList()) {
|
||||
if (query.refetchOnReconnect == false || !query.enabled) continue;
|
||||
await query.refetch();
|
||||
await Future.delayed(refetchOnReconnectDelay);
|
||||
}
|
||||
for (final infiniteQuery in this.cache.infiniteQueries.toList()) {
|
||||
if (infiniteQuery.refetchOnReconnect == false ||
|
||||
!infiniteQuery.enabled) continue;
|
||||
await infiniteQuery.refetchPages();
|
||||
await Future.delayed(refetchOnReconnectDelay);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (kIsMobile) {
|
||||
SystemChannels.lifecycle.setMessageHandler((msg) async {
|
||||
if (msg == 'AppLifecycleState.resumed') {
|
||||
if (_canNotRefetchAfterWeGotTheApp) return null;
|
||||
for (final query in this.cache.queries.toList()) {
|
||||
if (query.refetchOnApplicationResume == false || !query.enabled)
|
||||
continue;
|
||||
await query.refetch();
|
||||
await Future.delayed(refetchOnApplicationResumeDelay);
|
||||
}
|
||||
for (final infiniteQuery in this.cache.infiniteQueries.toList()) {
|
||||
if (infiniteQuery.refetchOnApplicationResume == false ||
|
||||
!infiniteQuery.enabled) continue;
|
||||
await infiniteQuery.refetchPages();
|
||||
await Future.delayed(refetchOnApplicationResumeDelay);
|
||||
}
|
||||
} else if (msg != null) {
|
||||
updateWeLostTheApp();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? _weLostTheAppAt;
|
||||
|
||||
/// Returns the number of query is currently fetching or refetching
|
||||
int get isFetching {
|
||||
return cache.queries.fold<int>(
|
||||
0,
|
||||
(acc, query) {
|
||||
if (query.isLoading || query.isRefetching) acc++;
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Provides the number of mutations that are running at the moment
|
||||
int get isMutating {
|
||||
return cache.mutations.fold<int>(
|
||||
0,
|
||||
(acc, mutation) {
|
||||
if (mutation.isLoading) acc++;
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool get _canNotRefetchAfterWeGotTheApp => (_weLostTheAppAt != null &&
|
||||
_weLostTheAppAt!.difference(DateTime.now()) <= cache.cacheTime);
|
||||
|
||||
@protected
|
||||
updateWeLostTheApp() {
|
||||
_weLostTheAppAt = DateTime.now();
|
||||
}
|
||||
|
||||
@protected
|
||||
notifyWindowFocused() async {
|
||||
if (kIsMobile || _canNotRefetchAfterWeGotTheApp) return;
|
||||
for (final query in this.cache.queries.toList()) {
|
||||
if (query.refetchOnWindowFocus == false || !query.enabled) continue;
|
||||
await query.refetch();
|
||||
await Future.delayed(refetchOnWindowFocusDelay);
|
||||
}
|
||||
for (final infiniteQuery in this.cache.infiniteQueries.toList()) {
|
||||
if (infiniteQuery.refetchOnWindowFocus == false || !infiniteQuery.enabled)
|
||||
continue;
|
||||
await infiniteQuery.refetchPages();
|
||||
await Future.delayed(refetchOnWindowFocusDelay);
|
||||
}
|
||||
}
|
||||
|
||||
void onQueriesUpdate<T extends Object, Outside>(
|
||||
void Function(Query<T, Outside> query) listener,
|
||||
) {
|
||||
cache.on((event, changes) {
|
||||
if (event == CacheEvent.query && changes is Query<T, Outside>) {
|
||||
listener(changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onMutationsUpdate<T extends Object, Outside>(
|
||||
void Function(Mutation<T, Outside> mutation) listener,
|
||||
) {
|
||||
cache.on((event, changes) {
|
||||
if (event == CacheEvent.mutation && changes is Mutation<T, Outside>) {
|
||||
listener(changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onInfiniteQueriesUpdate<T extends Object, Outside,
|
||||
PageParam extends Object>(
|
||||
void Function(InfiniteQuery<T, Outside, PageParam> infiniteQuery) listener,
|
||||
) {
|
||||
cache.on((event, changes) {
|
||||
if (event == CacheEvent.infiniteQuery &&
|
||||
changes is InfiniteQuery<T, Outside, PageParam>) {
|
||||
listener(changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
InfiniteQuery<T, Outside, PageParam>?
|
||||
getInfiniteQuery<T extends Object, Outside, PageParam extends Object>(
|
||||
String queryKey,
|
||||
) {
|
||||
return cache.infiniteQueries.firstWhereOrNull((infiniteQuery) {
|
||||
return infiniteQuery.queryKey == queryKey &&
|
||||
infiniteQuery is InfiniteQuery<T, Outside, PageParam>;
|
||||
})?.cast<InfiniteQuery<T, Outside, PageParam>>();
|
||||
}
|
||||
|
||||
/// Get a query by providing queryKey only
|
||||
///
|
||||
/// Useful for optimistic update or single query refetch
|
||||
Query<T, Outside>? getQuery<T extends Object, Outside>(String queryKey) {
|
||||
return cache.queries.firstWhereOrNull((query) {
|
||||
return query.queryKey == queryKey && query is Query<T, Outside>;
|
||||
})?.cast<Query<T, Outside>>();
|
||||
}
|
||||
|
||||
/// Get a mutation by providing mutationKey only
|
||||
///
|
||||
/// Useful for mutation resets
|
||||
Mutation<T, V>? getMutation<T extends Object, V>(String mutationKey) {
|
||||
return cache.mutations.firstWhereOrNull((mutation) {
|
||||
return mutation.mutationKey == mutationKey && mutation is Mutation<T, V>;
|
||||
})?.cast<Mutation<T, V>>();
|
||||
}
|
||||
|
||||
/// Sets [Query]'s data manually
|
||||
///
|
||||
/// Mostly used in combination with [onMutate] callback of
|
||||
/// [MutationBuilder] & [useMutation] for optimistic updates
|
||||
void setQueryData<T extends Object, Outside>(
|
||||
String queryKey,
|
||||
QueryUpdateFunction<T> updateCb,
|
||||
) {
|
||||
getQuery<T, Outside>(queryKey)?.setQueryData(updateCb);
|
||||
}
|
||||
|
||||
/// Sets [InfiniteQuery]'s data manually
|
||||
///
|
||||
/// Mostly used in combination with [onMutate] callback of
|
||||
/// [MutationBuilder] & [useMutation] for optimistic updates
|
||||
void
|
||||
setInfiniteQueryData<T extends Object, Outside, PageParam extends Object>(
|
||||
String queryKey,
|
||||
QueryUpdateFunction<Map<PageParam, T?>> updateCb,
|
||||
) {
|
||||
getInfiniteQuery<T, Outside, PageParam>(queryKey)?.setQueryData(updateCb);
|
||||
}
|
||||
|
||||
/// resets all the queries matching the passed List of queryKeys
|
||||
///
|
||||
/// If an empty list of [queryKeys] is passed then all of the queries
|
||||
/// will be reset
|
||||
void resetQueries(List<String> queryKeys) {
|
||||
for (final query in cache.queries) {
|
||||
if (queryKeys.isNotEmpty && !queryKeys.contains(query.queryKey)) continue;
|
||||
query.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// makes all the queries matching the passed List of queryKeys stale
|
||||
///
|
||||
/// If an empty list of [queryKeys] is passed then all of the queries
|
||||
/// will be invalidated
|
||||
void invalidateQueries(List<String> queryKeys) {
|
||||
for (final query in cache.queries) {
|
||||
if (queryKeys.isNotEmpty && queryKeys.contains(query.queryKey)) continue;
|
||||
query.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// refetches all the queries matching the passed List of queryKeys
|
||||
///
|
||||
/// If an empty list of [queryKeys] is passed then all of the queries
|
||||
/// will be refetched
|
||||
Future<void> refetchQueries(List<String> queryKeys) async {
|
||||
for (final query in cache.queries) {
|
||||
if (queryKeys.isNotEmpty && queryKeys.contains(query.queryKey)) continue;
|
||||
await query.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all the queries matching the passed List of queryKeys
|
||||
/// from the [QueryCache]
|
||||
int removeQueries(List<String> queryKeys) {
|
||||
int count = 0;
|
||||
for (final query in cache.queries) {
|
||||
if (queryKeys.isEmpty || !queryKeys.contains(query.queryKey)) continue;
|
||||
cache.removeQuery(query);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Query<T, Outside> _createQueryWithDefaults<T extends Object, Outside>(
|
||||
QueryJob<T, Outside> options,
|
||||
Outside externalData, [
|
||||
T? previousData,
|
||||
]) {
|
||||
final query = Query<T, Outside>.fromOptions(
|
||||
options,
|
||||
externalData: externalData,
|
||||
previousData: previousData,
|
||||
);
|
||||
query.updateDefaultOptions(
|
||||
cacheTime: cache.cacheTime,
|
||||
staleTime: staleTime,
|
||||
refetchInterval: refetchInterval,
|
||||
refetchOnMount: refetchOnMount,
|
||||
refetchOnReconnect: refetchOnReconnect,
|
||||
refetchOnApplicationResume: refetchOnApplicationResume,
|
||||
refetchOnWindowFocus: refetchOnWindowFocus,
|
||||
);
|
||||
return query;
|
||||
}
|
||||
|
||||
InfiniteQuery<T, Outside, PageParam> _createInfiniteQueryWithDefaults<
|
||||
T extends Object, Outside, PageParam extends Object>(
|
||||
InfiniteQueryJob<T, Outside, PageParam> options,
|
||||
Outside externalData,
|
||||
) {
|
||||
final infiniteQuery = InfiniteQuery<T, Outside, PageParam>.fromOptions(
|
||||
options,
|
||||
externalData: externalData,
|
||||
);
|
||||
infiniteQuery.updateDefaultOptions(
|
||||
cacheTime: cache.cacheTime,
|
||||
staleTime: staleTime,
|
||||
refetchInterval: refetchInterval,
|
||||
refetchOnMount: refetchOnMount,
|
||||
refetchOnReconnect: refetchOnReconnect,
|
||||
refetchOnApplicationResume: refetchOnApplicationResume,
|
||||
refetchOnWindowFocus: refetchOnWindowFocus,
|
||||
);
|
||||
return infiniteQuery;
|
||||
}
|
||||
|
||||
/// Creates/Updates a [Query] with the provided [QueryJob] and it's
|
||||
/// [externalData] and listeners and mounts the [QueryBuilder] or
|
||||
/// [useQuery] for the Query
|
||||
Query<T, Outside> addQuery<T extends Object, Outside>(
|
||||
QueryJob<T, Outside> queryJob, {
|
||||
required Outside externalData,
|
||||
required ValueKey<String> key,
|
||||
final QueryListener<T>? onData,
|
||||
final QueryListener<dynamic>? onError,
|
||||
final T? previousData,
|
||||
}) {
|
||||
final prevQuery = getQuery<T, Outside>(queryJob.queryKey);
|
||||
if (prevQuery != null) {
|
||||
// run the query if its still not called or if externalData has
|
||||
// changed
|
||||
if (!isShallowEqual(
|
||||
prevQuery.prevUsedExternalData,
|
||||
externalData,
|
||||
)) {
|
||||
prevQuery.setExternalData(externalData);
|
||||
}
|
||||
prevQuery.mount(key);
|
||||
if (onData != null) prevQuery.addDataListener(onData);
|
||||
if (onError != null) prevQuery.addErrorListener(onError);
|
||||
// mounting the widget that is using the query in the prevQuery
|
||||
return prevQuery;
|
||||
}
|
||||
final query = _createQueryWithDefaults<T, Outside>(
|
||||
queryJob,
|
||||
externalData,
|
||||
previousData,
|
||||
);
|
||||
if (onData != null) query.addDataListener(onData);
|
||||
if (onError != null) query.addErrorListener(onError);
|
||||
query.mount(key);
|
||||
cache.addQuery(query);
|
||||
return query;
|
||||
}
|
||||
|
||||
/// Creates/Updates a [InfiniteQuery] with the provided
|
||||
/// [InfiniteQueryJob] and it's [externalData] and listeners and mounts
|
||||
/// the [InfiniteQueryBuilder] or [useInfiniteQuery] for the it
|
||||
InfiniteQuery<T, Outside, PageParam>
|
||||
addInfiniteQuery<T extends Object, Outside, PageParam extends Object>(
|
||||
InfiniteQueryJob<T, Outside, PageParam> infiniteQueryJob, {
|
||||
required Outside externalData,
|
||||
required ValueKey<String> key,
|
||||
final InfiniteQueryListeners<T, PageParam>? onData,
|
||||
final InfiniteQueryListeners<dynamic, PageParam>? onError,
|
||||
}) {
|
||||
final prevInfiniteQuery =
|
||||
getInfiniteQuery<T, Outside, PageParam>(infiniteQueryJob.queryKey);
|
||||
if (prevInfiniteQuery != null) {
|
||||
// run the query if its still not called or if externalData has
|
||||
// changed
|
||||
if (!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;
|
||||
}
|
||||
|
||||
final infiniteQuery =
|
||||
_createInfiniteQueryWithDefaults<T, Outside, PageParam>(
|
||||
infiniteQueryJob,
|
||||
externalData,
|
||||
);
|
||||
if (onData != null) infiniteQuery.addDataListener(onData);
|
||||
if (onError != null) infiniteQuery.addErrorListener(onError);
|
||||
infiniteQuery.mount(key);
|
||||
cache.addInfiniteQuery(infiniteQuery);
|
||||
return infiniteQuery;
|
||||
}
|
||||
|
||||
/// Creates/Updates a [Mutation] with the provided
|
||||
/// [MutationJob] and it's listeners. Mounts
|
||||
/// the [MutationBuilder] or [useMutation] for the it
|
||||
Mutation<T, V> addMutation<T extends Object, V>(
|
||||
MutationJob<T, V> mutationJob, {
|
||||
final MutationListener<T, V>? onData,
|
||||
final MutationListener<dynamic, V>? onError,
|
||||
final MutationListenerReturnable<V, dynamic>? onMutate,
|
||||
required ValueKey<String> key,
|
||||
}) {
|
||||
final prevMutation = getMutation<T, V>(mutationJob.mutationKey);
|
||||
if (prevMutation != null) {
|
||||
if (onData != null) prevMutation.addDataListener(onData);
|
||||
if (onError != null) prevMutation.addErrorListener(onError);
|
||||
if (onMutate != null) prevMutation.addMutateListener(onMutate);
|
||||
prevMutation.mount(key);
|
||||
return prevMutation;
|
||||
} else {
|
||||
final mutation = Mutation<T, V>.fromOptions(
|
||||
mutationJob,
|
||||
);
|
||||
if (onData != null) mutation.addDataListener(onData);
|
||||
if (onError != null) mutation.addErrorListener(onError);
|
||||
if (onMutate != null) mutation.addMutateListener(onMutate);
|
||||
mutation.updateDefaultOptions(cacheTime: cache.cacheTime);
|
||||
mutation.mount(key);
|
||||
cache.addMutation(mutation);
|
||||
return mutation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates/Updates a [Query] with the provided [QueryJob] and it's
|
||||
/// [externalData] and listeners and mounts the [QueryBuilder] or
|
||||
/// [useQuery] for the Query
|
||||
///
|
||||
/// It also fetches/refetches the [Query] strategically/based on changes
|
||||
Future<T?> fetchQuery<T extends Object, Outside>(
|
||||
QueryJob<T, Outside> options, {
|
||||
required Outside externalData,
|
||||
final QueryListener<T>? onData,
|
||||
final QueryListener<dynamic>? onError,
|
||||
required ValueKey<String> key,
|
||||
}) async {
|
||||
final prevQuery = getQuery<T, Outside>(options.queryKey);
|
||||
if (prevQuery != null) {
|
||||
// run the query if its still not called or if externalData has
|
||||
// changed
|
||||
final hasExternalDataChanged = !isShallowEqual(
|
||||
prevQuery.prevUsedExternalData,
|
||||
externalData,
|
||||
);
|
||||
prevQuery.mount(key);
|
||||
if (onData != null) prevQuery.addDataListener(onData);
|
||||
if (onError != null) prevQuery.addErrorListener(onError);
|
||||
if (!prevQuery.hasData || hasExternalDataChanged) {
|
||||
if (hasExternalDataChanged) prevQuery.setExternalData(externalData);
|
||||
|
||||
return await prevQuery.refetch();
|
||||
}
|
||||
// mounting the widget that is using the query in the prevQuery
|
||||
return prevQuery.data;
|
||||
}
|
||||
|
||||
final query = _createQueryWithDefaults<T, Outside>(
|
||||
options,
|
||||
externalData,
|
||||
);
|
||||
|
||||
query.mount(key);
|
||||
cache.addQuery(query);
|
||||
return await query.fetch();
|
||||
}
|
||||
|
||||
/// Finds the closest instance of [QueryBowl] for the provided
|
||||
/// [BuildContext]
|
||||
static QueryBowl of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<QueryBowlScope>()!.bowl;
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple [InheritedWidget] that does the job of injecting [QueryBowl]
|
||||
/// into context
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return QueryBowlScope(
|
||||
/// bowl: QueryBowl(),
|
||||
/// child: MaterialApp(/*...other stuff...*/),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
class QueryBowlScope extends InheritedWidget {
|
||||
final QueryBowl bowl;
|
||||
|
||||
QueryBowlScope({
|
||||
required this.bowl,
|
||||
required Widget child,
|
||||
Key? key,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: MouseRegion(
|
||||
onEnter: (event) {
|
||||
bowl.notifyWindowFocused();
|
||||
},
|
||||
onExit: (event) {
|
||||
bowl.updateWeLostTheApp();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant oldWidget) => false;
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import 'package:fl_query/src/models/query_job.dart';
|
||||
import 'package:fl_query/src/query.dart';
|
||||
import 'package:fl_query/src/query_bowl.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class QueryBuilder<T extends Object, Outside> extends StatefulWidget {
|
||||
final Widget Function(BuildContext context, Query<T, Outside> query) builder;
|
||||
final QueryJob<T, Outside> job;
|
||||
final Outside externalData;
|
||||
|
||||
/// Called when the query returns new data, on query
|
||||
/// refetch or query gets expired
|
||||
final QueryListener<T>? onData;
|
||||
|
||||
/// Called when the query returns error
|
||||
final QueryListener<dynamic>? onError;
|
||||
|
||||
const QueryBuilder({
|
||||
required this.job,
|
||||
required this.externalData,
|
||||
required this.builder,
|
||||
this.onData,
|
||||
this.onError,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_QueryBuilderState<T, Outside> createState() =>
|
||||
_QueryBuilderState<T, Outside>();
|
||||
}
|
||||
|
||||
class _QueryBuilderState<T extends Object, Outside>
|
||||
extends State<QueryBuilder<T, Outside>> {
|
||||
Query<T, Outside>? query;
|
||||
late final ValueKey<String> uKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
uKey = ValueKey<String>(uuid.v4());
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
init();
|
||||
|
||||
QueryBowl.of(context).onQueriesUpdate<T, Outside>(
|
||||
(query) {
|
||||
if (query.queryKey != widget.job.queryKey) return;
|
||||
if (mounted)
|
||||
setState(() {
|
||||
this.query = query;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void init([T? previousData]) async {
|
||||
final bowl = QueryBowl.of(context);
|
||||
final prevQuery = bowl.getQuery<T, Outside>(widget.job.queryKey);
|
||||
query = bowl.addQuery<T, Outside>(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
previousData: previousData,
|
||||
key: uKey,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
);
|
||||
final hasExternalDataChanged = prevQuery != null
|
||||
? !isShallowEqual(
|
||||
prevQuery.externalData,
|
||||
prevQuery.prevUsedExternalData,
|
||||
)
|
||||
: !isShallowEqual(
|
||||
query!.externalData,
|
||||
query!.prevUsedExternalData,
|
||||
);
|
||||
|
||||
if (query!.fetched && hasExternalDataChanged) {
|
||||
await query!.refetch();
|
||||
} else if (!query!.fetched) {
|
||||
await query!.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
final bowl = QueryBowl.of(context);
|
||||
final hasOnErrorChanged =
|
||||
oldWidget.onError != widget.onError && oldWidget.onError != null;
|
||||
final hasOnDataChanged =
|
||||
oldWidget.onData != widget.onData && oldWidget.onData != null;
|
||||
|
||||
// re-init the query-builder when new queryJob is appended
|
||||
if (oldWidget.job.queryKey != widget.job.queryKey) {
|
||||
_queryDispose();
|
||||
|
||||
/// setting the new query's initial data as prev query's data
|
||||
/// when [job.keepPreviousData] is true and both are dynamic
|
||||
if (oldWidget.job.isDynamic &&
|
||||
widget.job.isDynamic &&
|
||||
oldWidget.job.keepPreviousData == true &&
|
||||
widget.job.keepPreviousData == true) {
|
||||
init(query?.data);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
} else if (!isShallowEqual(oldWidget.externalData, widget.externalData)) {
|
||||
if (widget.job.refetchOnExternalDataChange ??
|
||||
bowl.refetchOnExternalDataChange) {
|
||||
bowl.fetchQuery(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
key: uKey,
|
||||
);
|
||||
} else {
|
||||
bowl
|
||||
.getQuery(widget.job.queryKey)
|
||||
?.setExternalData(widget.externalData);
|
||||
}
|
||||
if (hasOnDataChanged) query?.removeDataListener(oldWidget.onData!);
|
||||
if (hasOnErrorChanged) query?.removeErrorListener(oldWidget.onError!);
|
||||
} else {
|
||||
if (hasOnDataChanged) {
|
||||
query?.removeDataListener(oldWidget.onData!);
|
||||
if (widget.onData != null) query?.addDataListener(widget.onData!);
|
||||
}
|
||||
if (hasOnErrorChanged) {
|
||||
query?.removeErrorListener(oldWidget.onError!);
|
||||
if (widget.onError != null) query?.addErrorListener(widget.onError!);
|
||||
}
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
_queryDispose() {
|
||||
query?.unmount(uKey);
|
||||
if (widget.onData != null) query?.removeDataListener(widget.onData!);
|
||||
if (widget.onError != null) query?.removeErrorListener(widget.onError!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_queryDispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (query == null) return Container();
|
||||
return widget.builder(context, query!);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query/src/base_operation.dart';
|
||||
|
||||
typedef ReadonlySet<E> = UnmodifiableSetView<E>;
|
||||
|
||||
enum CacheEvent {
|
||||
clearCache,
|
||||
query,
|
||||
infiniteQuery,
|
||||
mutation,
|
||||
}
|
||||
|
||||
typedef CacheUpdateListener<T> = void Function(CacheEvent event, T? changes);
|
||||
|
||||
class QueryCache {
|
||||
/// Removes inactive queries after provided duration of [cacheTime]
|
||||
final Duration cacheTime;
|
||||
|
||||
QueryCache({Duration? cacheTime})
|
||||
: cacheTime = cacheTime ?? const Duration(minutes: 5) {}
|
||||
|
||||
final Set<Query> _queries = {};
|
||||
final Set<InfiniteQuery> _infiniteQueries = {};
|
||||
final Set<Mutation> _mutations = {};
|
||||
|
||||
final Set<CacheUpdateListener<BaseOperation>> _listeners = {};
|
||||
|
||||
Set<Query> get queries => _queries;
|
||||
Set<InfiniteQuery> get infiniteQueries => _infiniteQueries;
|
||||
Set<Mutation> get mutations => _mutations;
|
||||
|
||||
ReadonlySet<CacheUpdateListener<BaseOperation>> get listeners =>
|
||||
UnmodifiableSetView(_listeners);
|
||||
|
||||
_notifyListeners<T extends Object>(CacheEvent event, changes) {
|
||||
_listeners.forEach((listener) => listener(event, changes));
|
||||
}
|
||||
|
||||
_listenToQueryChanges(Query query) {
|
||||
query.addListener(() {
|
||||
if (query.isInactive) {
|
||||
_queries.removeWhere((el) {
|
||||
if (el.queryKey != query.queryKey) {
|
||||
el.dispose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
_notifyListeners(CacheEvent.query, null);
|
||||
} else {
|
||||
_notifyListeners(CacheEvent.query, query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_listenToInfiniteQueryChanges(InfiniteQuery infiniteQuery) {
|
||||
infiniteQuery.addListener(() {
|
||||
if (infiniteQuery.isInactive) {
|
||||
_infiniteQueries.removeWhere((el) {
|
||||
if (el.queryKey != infiniteQuery.queryKey) {
|
||||
el.dispose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
_notifyListeners(CacheEvent.infiniteQuery, null);
|
||||
} else {
|
||||
_notifyListeners(CacheEvent.infiniteQuery, infiniteQuery);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_listenToMutationChanges(Mutation mutation) {
|
||||
mutation.addListener(() {
|
||||
if (mutation.isInactive) {
|
||||
_mutations.removeWhere(
|
||||
(el) {
|
||||
if (el.mutationKey != mutation.mutationKey) {
|
||||
el.dispose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
_notifyListeners(CacheEvent.mutation, null);
|
||||
} else {
|
||||
_notifyListeners(CacheEvent.mutation, mutation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void addQuery(Query query) {
|
||||
_queries.add(query);
|
||||
_listenToQueryChanges(query);
|
||||
_notifyListeners(CacheEvent.query, query);
|
||||
}
|
||||
|
||||
void addInfiniteQuery(InfiniteQuery infiniteQuery) {
|
||||
_infiniteQueries.add(infiniteQuery);
|
||||
_listenToInfiniteQueryChanges(infiniteQuery);
|
||||
_notifyListeners(CacheEvent.infiniteQuery, infiniteQuery);
|
||||
}
|
||||
|
||||
void addMutation(Mutation mutation) {
|
||||
_mutations.add(mutation);
|
||||
_listenToMutationChanges(mutation);
|
||||
_notifyListeners(CacheEvent.mutation, mutation);
|
||||
}
|
||||
|
||||
void removeQuery(Query query) {
|
||||
query.dispose();
|
||||
_queries.remove(query);
|
||||
_notifyListeners(CacheEvent.query, null);
|
||||
}
|
||||
|
||||
void removeInfiniteQuery(InfiniteQuery infiniteQuery) {
|
||||
infiniteQuery.dispose();
|
||||
_infiniteQueries.remove(infiniteQuery);
|
||||
_notifyListeners(CacheEvent.infiniteQuery, null);
|
||||
}
|
||||
|
||||
void removeMutation(Mutation mutation) {
|
||||
mutation.dispose();
|
||||
_mutations.remove(mutation);
|
||||
_notifyListeners(CacheEvent.mutation, null);
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
_infiniteQueries.forEach((el) => el.dispose());
|
||||
_queries.forEach((el) => el.dispose());
|
||||
_mutations.forEach((el) => el.dispose());
|
||||
_infiniteQueries.clear();
|
||||
_queries.clear();
|
||||
_mutations.clear();
|
||||
_notifyListeners(CacheEvent.clearCache, null);
|
||||
}
|
||||
|
||||
void on(CacheUpdateListener<BaseOperation?> listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
void off(CacheUpdateListener<BaseOperation?> listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:fl_query/src/query.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
Future<void> callQueryListeners<T>(Set<QueryListener<T>> listeners, T data) {
|
||||
return Future.wait(listeners.map(
|
||||
(listener) => Future.value(listener(data)),
|
||||
));
|
||||
// for (final listener in listeners) {
|
||||
// await listener(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(dynamic obj1, dynamic obj2) {
|
||||
if (obj1 == null && obj2 == null) return true;
|
||||
if ((obj1 == null && obj2 != null) || (obj2 == null && obj1 != null))
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
bool isConnectedToInternet(ConnectivityResult result) {
|
||||
return [
|
||||
ConnectivityResult.ethernet,
|
||||
ConnectivityResult.mobile,
|
||||
ConnectivityResult.wifi,
|
||||
].contains(result);
|
||||
}
|
||||
|
||||
String getVariable(String queryKey) {
|
||||
return queryKey.split("#").last;
|
||||
}
|
||||
|
||||
final kIsDesktop = kIsLinux || kIsWindows || kIsMacOS;
|
||||
|
||||
final kIsMobile = kIsAndroid || kIsIOS;
|
||||
|
||||
final kIsMacOS = kIsWeb ? false : Platform.isMacOS;
|
||||
|
||||
final kIsLinux = kIsWeb ? false : Platform.isLinux;
|
||||
final kIsAndroid = kIsWeb ? false : Platform.isAndroid;
|
||||
final kIsIOS = kIsWeb ? false : Platform.isIOS;
|
||||
final kIsWindows = kIsWeb ? false : Platform.isWindows;
|
||||
Reference in New Issue
Block a user