Depended queries support added
This commit is contained in:
@@ -60,6 +60,6 @@ abstract class BaseOperation<Data, StatusType> extends ChangeNotifier {
|
||||
bool get isLoading;
|
||||
bool get isIdle;
|
||||
bool get isInactive => mounts.isEmpty;
|
||||
bool get hasData => isSuccess && data != null;
|
||||
bool get hasError => isError && error != null;
|
||||
bool get hasData => data != null;
|
||||
bool get hasError => error != null;
|
||||
}
|
||||
|
||||
@@ -17,106 +17,67 @@ Query<T, Outside> useQuery<T extends Object, Outside>({
|
||||
QueryListener<dynamic>? onError,
|
||||
List<Object?>? keys,
|
||||
}) {
|
||||
return use(_UseQuery<T, Outside>(
|
||||
externalData: externalData,
|
||||
job: job,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
keys: keys,
|
||||
));
|
||||
}
|
||||
final context = useContext();
|
||||
QueryBowl queryBowl = QueryBowl.of(context);
|
||||
final ValueKey<String> uKey = useMemoized(() => ValueKey(uuid.v4()), []);
|
||||
Query<T, Outside> query = useMemoized(
|
||||
() => Query.fromOptions(
|
||||
job,
|
||||
externalData: externalData,
|
||||
queryBowl: queryBowl,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
),
|
||||
[]);
|
||||
|
||||
class _UseQuery<T extends Object, Outside> extends Hook<Query<T, Outside>> {
|
||||
final QueryJob<T, Outside> job;
|
||||
final Outside externalData;
|
||||
final oldExternalData = usePrevious(externalData);
|
||||
final oldOnData = usePrevious(onData);
|
||||
final oldOnError = usePrevious(onError);
|
||||
|
||||
/// 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 _UseQuery({
|
||||
required this.job,
|
||||
required this.externalData,
|
||||
this.onData,
|
||||
this.onError,
|
||||
super.keys,
|
||||
});
|
||||
|
||||
@override
|
||||
HookState<Query<T, Outside>, Hook<Query<T, Outside>>> createState() =>
|
||||
_UseQueryHookState();
|
||||
}
|
||||
|
||||
class _UseQueryHookState<T extends Object, Outside>
|
||||
extends HookState<Query<T, Outside>, _UseQuery<T, Outside>> {
|
||||
late QueryBowl queryBowl;
|
||||
late final ValueKey<String> uKey;
|
||||
late Query<T, Outside> query;
|
||||
|
||||
@override
|
||||
void initHook() {
|
||||
super.initHook();
|
||||
uKey = ValueKey<String>(uuid.v4());
|
||||
query = Query<T, Outside>.fromOptions(
|
||||
hook.job,
|
||||
externalData: hook.externalData,
|
||||
useEffect(() {
|
||||
queryBowl.addQuery<T, Outside>(
|
||||
query,
|
||||
key: uKey,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
query = QueryBowl.of(context).addQuery<T, Outside>(
|
||||
query,
|
||||
key: uKey,
|
||||
onData: hook.onData,
|
||||
onError: hook.onError,
|
||||
);
|
||||
final hasExternalDataChanged = query.externalData != null &&
|
||||
query.prevUsedExternalData != null &&
|
||||
!isShallowEqual(query.externalData!, query.prevUsedExternalData!);
|
||||
(query.fetched && query.refetchOnMount == true) || hasExternalDataChanged
|
||||
? await query.refetch()
|
||||
: await query.fetch();
|
||||
});
|
||||
}
|
||||
final hasExternalDataChanged = query.externalData != null &&
|
||||
query.prevUsedExternalData != null &&
|
||||
!isShallowEqual(query.externalData!, query.prevUsedExternalData!);
|
||||
(query.fetched && query.refetchOnMount == true) || hasExternalDataChanged
|
||||
? query.refetch()
|
||||
: query.fetch();
|
||||
|
||||
@override
|
||||
void didUpdateHook(_UseQuery<T, Outside> oldHook) {
|
||||
if (oldHook.externalData != null &&
|
||||
hook.externalData != null &&
|
||||
!isShallowEqual(oldHook.externalData!, hook.externalData!)) {
|
||||
return () {
|
||||
query.unmount(uKey);
|
||||
if (onData != null) query.onDataListeners.remove(onData);
|
||||
if (onError != null) query.onErrorListeners.remove(onError);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
if (oldExternalData != null &&
|
||||
externalData != null &&
|
||||
!isShallowEqual(oldExternalData, externalData)) {
|
||||
QueryBowl.of(context).fetchQuery(
|
||||
hook.job,
|
||||
externalData: hook.externalData,
|
||||
onData: hook.onData,
|
||||
onError: hook.onError,
|
||||
job,
|
||||
externalData: externalData,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
key: uKey,
|
||||
);
|
||||
} else {
|
||||
if (oldHook.onData != hook.onData && oldHook.onData != null) {
|
||||
query.onDataListeners.remove(oldHook.onData);
|
||||
if (hook.onData != null) query.onDataListeners.add(hook.onData!);
|
||||
if (oldOnData != onData && oldOnData != null) {
|
||||
query.onDataListeners.remove(oldOnData);
|
||||
if (onData != null) query.onDataListeners.add(onData);
|
||||
}
|
||||
if (oldHook.onError != hook.onError && oldHook.onError != null) {
|
||||
query.onErrorListeners.remove(oldHook.onError);
|
||||
if (hook.onError != null) query.onErrorListeners.add(hook.onError!);
|
||||
if (oldOnError != onError && oldOnError != null) {
|
||||
query.onErrorListeners.remove(oldOnError);
|
||||
if (onError != null) query.onErrorListeners.add(onError);
|
||||
}
|
||||
}
|
||||
super.didUpdateHook(oldHook);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
query.unmount(uKey);
|
||||
if (hook.onData != null) query.onDataListeners.remove(hook.onData);
|
||||
if (hook.onError != null) query.onErrorListeners.remove(hook.onError);
|
||||
}
|
||||
|
||||
@override
|
||||
Query<T, Outside> build(BuildContext context) {
|
||||
queryBowl = QueryBowl.of(context);
|
||||
return queryBowl.getQuery<T, Outside>(query.queryKey) ?? query;
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useQuery';
|
||||
return queryBowl.getQuery<T, Outside>(job.queryKey) ?? query;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import 'package:fl_query/src/query.dart';
|
||||
|
||||
/// How to make dependent Queries?
|
||||
///
|
||||
/// Pass a [QueryBowl] class/object to [task] that contains a method
|
||||
/// [QueryBowl.dependOnQuery] that takes a [QueryJob] and uses that to get
|
||||
/// the appropriate query for it or if it doesn't exist creates a new
|
||||
/// instance. It listens to the changes of [Query] of the passed
|
||||
/// [QueryJob] & calls the [notifyListener] method of running [Query]
|
||||
///
|
||||
/// If shown briefly
|
||||
///
|
||||
/// ```data
|
||||
/// task: (queryKey, external, queryBowl){
|
||||
/// final dependentQuery = queryBowl.dependOnQuery(dependentQueryJob);
|
||||
/// return someAsyncTask();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
class QueryJob<T extends Object, Outside> {
|
||||
// all params
|
||||
String _queryKey;
|
||||
|
||||
@@ -2,7 +2,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/base_operation.dart';
|
||||
import 'package:fl_query/src/models/query_job.dart';
|
||||
import 'package:fl_query/src/query_bowl.dart';
|
||||
import 'package:fl_query/src/utils.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
enum QueryStatus {
|
||||
/// in times when an error occurs
|
||||
@@ -23,7 +26,11 @@ enum QueryStatus {
|
||||
refetching;
|
||||
}
|
||||
|
||||
typedef QueryTaskFunction<T, Outside> = FutureOr<T> Function(String, Outside);
|
||||
typedef QueryTaskFunction<T extends Object, Outside> = FutureOr<T> Function(
|
||||
String queryKey,
|
||||
Outside externalData,
|
||||
Query<T, Outside> self,
|
||||
);
|
||||
|
||||
typedef QueryListener<T> = FutureOr<void> Function(T);
|
||||
|
||||
@@ -64,6 +71,8 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
|
||||
Timer? _refetchIntervalTimer;
|
||||
|
||||
final QueryBowl queryBowl;
|
||||
|
||||
Query({
|
||||
required this.queryKey,
|
||||
required this.task,
|
||||
@@ -72,6 +81,7 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
required Outside externalData,
|
||||
required super.retries,
|
||||
required super.retryDelay,
|
||||
required this.queryBowl,
|
||||
this.refetchOnMount,
|
||||
this.refetchInterval,
|
||||
this.enabled = true,
|
||||
@@ -95,6 +105,7 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
|
||||
Query.fromOptions(
|
||||
QueryJob<T, Outside> options, {
|
||||
required this.queryBowl,
|
||||
required Outside externalData,
|
||||
QueryListener<T>? onData,
|
||||
QueryListener<dynamic>? onError,
|
||||
@@ -136,7 +147,11 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
Future<void> _execute() async {
|
||||
try {
|
||||
retryAttempts = 0;
|
||||
data = await task(queryKey, _externalData);
|
||||
data = await task(
|
||||
queryKey,
|
||||
_externalData,
|
||||
this,
|
||||
);
|
||||
_prevUsedExternalData = _externalData;
|
||||
updatedAt = DateTime.now();
|
||||
status = QueryStatus.success;
|
||||
@@ -157,7 +172,11 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
while (retryAttempts <= retries) {
|
||||
await Future.delayed(retryDelay);
|
||||
try {
|
||||
data = await task(queryKey, _externalData);
|
||||
data = await task(
|
||||
queryKey,
|
||||
_externalData,
|
||||
this,
|
||||
);
|
||||
_prevUsedExternalData = _externalData;
|
||||
status = QueryStatus.success;
|
||||
for (final onData in onDataListeners) {
|
||||
@@ -182,7 +201,9 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
}
|
||||
|
||||
Future<T?> fetch() async {
|
||||
if (!enabled) return null;
|
||||
/// if isLoading/isRefetching is true that means its already fetching/
|
||||
/// refetching. So [_execute] again can create a race condition
|
||||
if (!enabled || isLoading || isRefetching) return null;
|
||||
if (hasData) {
|
||||
return data;
|
||||
}
|
||||
@@ -195,13 +216,14 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
}
|
||||
|
||||
Future<T?> refetch() async {
|
||||
// cannot let run multiple refetch at the same time. It can cause
|
||||
// race-condition
|
||||
if (isRefetching) return null;
|
||||
/// if isLoading/isRefetching is true that means its already fetching/
|
||||
/// refetching. So [_execute] again can create a race condition
|
||||
if (isRefetching || isLoading) return null;
|
||||
status = QueryStatus.refetching;
|
||||
refetchCount++;
|
||||
// disabling the lazy query bound when query was actually called
|
||||
if (!enabled) enabled = false;
|
||||
if (!enabled) enabled = true;
|
||||
if (enabled && !fetched) await fetch();
|
||||
notifyListeners();
|
||||
return await _execute().then((_) => data);
|
||||
}
|
||||
@@ -239,6 +261,11 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
fetched = false;
|
||||
status = QueryStatus.idle;
|
||||
retryAttempts = 0;
|
||||
for (final queryEntry in _dependencyQueries.entries) {
|
||||
queryEntry.value.unmount(queryEntry.key);
|
||||
queryEntry.value.removeListener(refetch);
|
||||
}
|
||||
_dependencyQueries = {};
|
||||
onDataListeners.clear();
|
||||
onErrorListeners.clear();
|
||||
mounts.clear();
|
||||
@@ -268,6 +295,42 @@ class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Map<ValueKey<String>, Query> _dependencyQueries = {};
|
||||
|
||||
/// only usable inside any task method
|
||||
Query<T, Outside> dependOnQuery<T extends Object, Outside>(
|
||||
QueryJob<T, Outside> job, {
|
||||
required Outside externalData,
|
||||
}) {
|
||||
final key = ValueKey(uuid.v4());
|
||||
final query = queryBowl.addQuery(
|
||||
Query<T, Outside>.fromOptions(
|
||||
job,
|
||||
externalData: externalData,
|
||||
queryBowl: queryBowl,
|
||||
),
|
||||
key: key,
|
||||
);
|
||||
// removing listener if it was already hooked to it previously
|
||||
query.removeListener(refetch);
|
||||
query.addListener(refetch);
|
||||
final uKey =
|
||||
_dependencyQueries.keys.firstWhereOrNull((k) => k.value == key.value) ??
|
||||
key;
|
||||
_dependencyQueries[uKey] = query;
|
||||
if (!query.fetched) query.fetch();
|
||||
return query;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final queryEntry in _dependencyQueries.entries) {
|
||||
queryEntry.value.unmount(queryEntry.key);
|
||||
queryEntry.value.removeListener(refetch);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get isStale {
|
||||
/// when [_staleTime] is [Duration.zero], the query will always be
|
||||
/// stale & will never refetch in the background. But can be inactive
|
||||
|
||||
@@ -30,7 +30,7 @@ class QueryBowlScope extends StatefulWidget {
|
||||
const QueryBowlScope({
|
||||
required this.child,
|
||||
this.staleTime = Duration.zero,
|
||||
this.cacheTime = const Duration(minutes: 5),
|
||||
this.cacheTime = const Duration(seconds: 5),
|
||||
this.refetchInterval = Duration.zero,
|
||||
this.refetchOnMount = false,
|
||||
this.refetchOnReconnect = true,
|
||||
@@ -79,9 +79,23 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
|
||||
}
|
||||
|
||||
void updateQueries(Query query) {
|
||||
// checking & not including inactive queries
|
||||
// basically garbage collecting queries
|
||||
if (query.isInactive) {
|
||||
/// there's a bug currently,
|
||||
/// where if somehow a [Query] (queryA) is depending on another
|
||||
/// [Query] (queryB) & queryA has become inactive so its getting
|
||||
/// disposed but at the same moment queryB got refetched will make
|
||||
/// queryB's [BaseOperation.unmount] to throw [RangeError] for
|
||||
/// calling [ChangeNotifier.notifyListener] after [cacheDelay]
|
||||
/// inside [Timer.periodic] callback
|
||||
///
|
||||
/// To mitigate this, [Query.reset] is used instead of using
|
||||
/// [Query.dispose] as it doesn't call [super.dispose]
|
||||
query.reset();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
// checking & not including inactive queries
|
||||
// basically garbage collecting queries
|
||||
queries = Set.from(
|
||||
query.isInactive
|
||||
? queries.where((el) => el.queryKey != query.queryKey)
|
||||
@@ -240,6 +254,7 @@ class QueryBowl extends InheritedWidget {
|
||||
final query = Query<T, Outside>.fromOptions(
|
||||
options,
|
||||
externalData: externalData,
|
||||
queryBowl: this,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
);
|
||||
|
||||
@@ -34,29 +34,30 @@ class _QueryBuilderState<T extends Object, Outside>
|
||||
extends State<QueryBuilder<T, Outside>> {
|
||||
late QueryBowl queryBowl;
|
||||
late final ValueKey<String> uKey;
|
||||
late Query<T, Outside> query;
|
||||
Query<T, Outside>? query;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
uKey = ValueKey<String>(uuid.v4());
|
||||
query = Query<T, Outside>.fromOptions(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
query = QueryBowl.of(context).addQuery<T, Outside>(
|
||||
query,
|
||||
Query<T, Outside>.fromOptions(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
queryBowl: QueryBowl.of(context),
|
||||
),
|
||||
key: uKey,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
);
|
||||
final hasExternalDataChanged = query.externalData != null &&
|
||||
query.prevUsedExternalData != null &&
|
||||
!isShallowEqual(query.externalData!, query.prevUsedExternalData!);
|
||||
(query.fetched && query.refetchOnMount == true) || hasExternalDataChanged
|
||||
? await query.refetch()
|
||||
: await query.fetch();
|
||||
final hasExternalDataChanged = query!.externalData != null &&
|
||||
query!.prevUsedExternalData != null &&
|
||||
!isShallowEqual(query!.externalData!, query!.prevUsedExternalData!);
|
||||
(query!.fetched && query!.refetchOnMount == true) ||
|
||||
hasExternalDataChanged
|
||||
? await query!.refetch()
|
||||
: await query!.fetch();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,12 +75,13 @@ class _QueryBuilderState<T extends Object, Outside>
|
||||
);
|
||||
} else {
|
||||
if (oldWidget.onData != widget.onData && oldWidget.onData != null) {
|
||||
query.onDataListeners.remove(oldWidget.onData);
|
||||
if (widget.onData != null) query.onDataListeners.add(widget.onData!);
|
||||
query?.onDataListeners.remove(oldWidget.onData);
|
||||
if (widget.onData != null) query?.onDataListeners.add(widget.onData!);
|
||||
}
|
||||
if (oldWidget.onError != widget.onError && oldWidget.onError != null) {
|
||||
query.onErrorListeners.remove(oldWidget.onError);
|
||||
if (widget.onError != null) query.onErrorListeners.add(widget.onError!);
|
||||
query?.onErrorListeners.remove(oldWidget.onError);
|
||||
if (widget.onError != null)
|
||||
query?.onErrorListeners.add(widget.onError!);
|
||||
}
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -87,16 +89,18 @@ class _QueryBuilderState<T extends Object, Outside>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
query.unmount(uKey);
|
||||
if (widget.onData != null) query.onDataListeners.remove(widget.onData);
|
||||
if (widget.onError != null) query.onErrorListeners.remove(widget.onError);
|
||||
query?.unmount(uKey);
|
||||
if (widget.onData != null) query?.onDataListeners.remove(widget.onData);
|
||||
if (widget.onError != null) query?.onErrorListeners.remove(widget.onError);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
queryBowl = QueryBowl.of(context);
|
||||
final latestQuery = queryBowl.getQuery<T, Outside>(query.queryKey) ?? query;
|
||||
final latestQuery =
|
||||
queryBowl.getQuery<T, Outside>(widget.job.queryKey) ?? query;
|
||||
if (latestQuery == null) return Container();
|
||||
return widget.builder(context, latestQuery);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user