Depended queries support added

This commit is contained in:
Kingkor Roy Tirtho
2022-06-22 17:56:24 +06:00
parent 8c53f1b0fe
commit 73711ee77d
10 changed files with 234 additions and 127 deletions
@@ -0,0 +1,36 @@
import 'package:example/main.dart';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart';
final dependentQueryJob = QueryJob<String, void>(
queryKey: "dependent-query-job",
task: (queryKey, externalData, query) {
final successQuery =
query.dependOnQuery<String, void>(successJob, externalData: null);
final failedQuery =
query.dependOnQuery<String, void>(failedJob, externalData: null);
if (failedQuery.hasError) return failedQuery.error.toString();
if (successQuery.hasData) return successQuery.data!;
return "No data from success query yet";
});
class DependentQueryExample extends StatelessWidget {
const DependentQueryExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: QueryBuilder<String, void>(
job: dependentQueryJob,
externalData: null,
builder: (context, query) {
if (query.isLoading || query.isRefetching || !query.hasData) {
return const CircularProgressIndicator();
}
return Text(query.data!);
},
),
);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
final lazyQueryJob = QueryJob<String, String>(
queryKey: "non_enabled_query",
enabled: false,
task: (queryKey, data) {
task: (queryKey, data, _) {
return Future.delayed(const Duration(milliseconds: 500),
() => "Hello from $queryKey with $data");
});
+13 -2
View File
@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:example/another_component.dart';
import 'package:example/dependent_query_example.dart';
import 'package:example/hooks_example.dart';
import 'package:example/lazy_query.dart';
import 'package:example/mutation_example.dart';
@@ -34,13 +35,13 @@ class MyApp extends StatelessWidget {
final successJob = QueryJob<String, void>(
queryKey: "greetings",
task: (queryKey, _) => Future.delayed(const Duration(seconds: 2),
task: (queryKey, _, __) => Future.delayed(const Duration(seconds: 2),
() => "Welcome ($queryKey) ${Random.secure().nextInt(100)}"),
);
final failedJob = QueryJob<String, void>(
queryKey: "failure",
task: (queryKey, _) => Random().nextBool()
task: (queryKey, _, __) => Random().nextBool()
? Future.error("[$queryKey] Failed for unknown reason")
: Future.value(
"Success, you'll get slowly ${Random().nextInt(100)}!",
@@ -178,6 +179,16 @@ class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
);
},
),
ElevatedButton(
child: const Text("Dependent Query Example"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DependentQueryExample(),
),
);
},
),
const AnotherComponent(),
],
),
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
final jobWithExternalData = QueryJob<String, String>(
queryKey: "external_data",
cacheTime: const Duration(seconds: 10),
task: (queryKey, data) {
task: (queryKey, data, _) {
return Future.delayed(const Duration(milliseconds: 500),
() => "Hello from $queryKey with $data");
});
@@ -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;
}
+51 -90
View File
@@ -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;
+71 -8
View File
@@ -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
+18 -3
View File
@@ -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,
);
+24 -20
View File
@@ -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);
}
}