Internal API change

now query & mutation share same base class
queries are now self-refetchable
queryJobs & mutationJobs now support dynamic key allowing dynamic Query/Mutation creation
query status & mutation status are now just on point & accurate
This commit is contained in:
Kingkor Roy Tirtho
2022-06-21 17:16:06 +06:00
parent 634bb3f912
commit e0c70d153f
13 changed files with 417 additions and 184 deletions
+21 -3
View File
@@ -53,7 +53,25 @@ class MyHomePage extends StatefulWidget {
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print("LIFE CYCLE STATE: $state");
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -68,7 +86,7 @@ class _MyHomePageState extends State<MyHomePage> {
job: successJob,
externalData: null,
builder: (context, query) {
if (query.isLoading || query.isRefetching) {
if (!query.hasData || query.isLoading || query.isRefetching) {
return const CircularProgressIndicator();
}
return TextButton(
@@ -83,7 +101,7 @@ class _MyHomePageState extends State<MyHomePage> {
job: successJob,
externalData: null,
builder: (context, query) {
if (query.isLoading || query.isRefetching) {
if (!query.hasData || query.isLoading || query.isRefetching) {
return const CircularProgressIndicator();
}
return ElevatedButton(
+1 -2
View File
@@ -1,8 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:fl_query/models/mutation_job.dart';
import 'package:fl_query/mutation_builder.dart';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
@@ -22,7 +22,7 @@ class QueryWithExternalData extends StatelessWidget {
job: jobWithExternalData,
externalData: (Random().nextDouble() * 200).toString(),
builder: (context, query) {
if (query.isLoading || query.isLoading || query.data == null) {
if (query.isLoading || query.isRefetching || !query.hasData) {
return const CircularProgressIndicator();
}
return Container(
@@ -32,6 +32,7 @@ class QueryWithExternalData extends StatelessWidget {
shape: BoxShape.circle,
color: Colors.blue,
),
child: Text(query.externalData),
);
},
),
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/widgets.dart';
abstract class BaseOperation<Data, StatusType> 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;
// all properties
Data? data;
dynamic error;
StatusType status;
/// total count of how many times the query retried to get a successful
/// result
int retryAttempts = 0;
DateTime updatedAt;
@protected
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,
required this.status,
this.data,
}) : updatedAt = DateTime.now();
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;
bool get isSuccess;
bool get isError;
bool get isLoading;
bool get isIdle;
bool get isInactive => mounts.isEmpty;
bool get hasData => isSuccess && data != null;
bool get hasError => isError && error != null;
}
+3
View File
@@ -3,4 +3,7 @@ library fl_query;
export 'query.dart';
export 'query_bowl.dart';
export 'query_builder.dart';
export 'mutation.dart';
export 'mutation_builder.dart';
export 'models/query_job.dart';
export 'models/mutation_job.dart';
+29 -3
View File
@@ -1,17 +1,43 @@
import 'package:fl_query/mutation.dart';
class MutationJob<T extends Object, V> {
final String mutationKey;
String _mutationKey;
MutationTaskFunction<T, V> task;
final int? retries;
final Duration? retryDelay;
final Duration? cacheTime;
MutationJob({
required this.mutationKey,
required String mutationKey,
required this.task,
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,
}) {
return (String mutationKey) {
if (preMutationKey != null) mutationKey = "$preMutationKey#$mutationKey";
return MutationJob<T, V>(
mutationKey: mutationKey,
task: task,
retries: retries,
retryDelay: retryDelay,
cacheTime: cacheTime,
);
};
}
}
+46 -5
View File
@@ -2,7 +2,7 @@ import 'package:fl_query/query.dart';
class QueryJob<T extends Object, Outside> {
// all params
final String queryKey;
String _queryKey;
QueryTaskFunction<T, Outside> task;
final int? retries;
final Duration? retryDelay;
@@ -13,11 +13,14 @@ class QueryJob<T extends Object, Outside> {
final bool? enabled;
// got from global options
final Duration? staleTime;
final Duration? cacheTime;
bool? refetchOnMount;
Duration? staleTime;
Duration? cacheTime;
Duration? refetchInterval;
QueryJob({
required this.queryKey,
required String queryKey,
required this.task,
this.retries,
this.retryDelay,
@@ -25,5 +28,43 @@ class QueryJob<T extends Object, Outside> {
this.staleTime,
this.cacheTime,
this.enabled,
});
this.refetchInterval,
this.refetchOnMount,
}) : _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,
}) {
return (String queryKey) {
if (preQueryKey != null) queryKey = "$preQueryKey#$queryKey";
return QueryJob<T, Outside>(
queryKey: queryKey,
task: task,
retries: retries,
retryDelay: retryDelay,
initialData: initialData,
staleTime: staleTime,
cacheTime: cacheTime,
enabled: enabled,
refetchInterval: refetchInterval,
refetchOnMount: refetchOnMount,
);
};
}
}
+46 -67
View File
@@ -1,40 +1,24 @@
import 'dart:async';
import 'package:fl_query/base_operation.dart';
import 'package:fl_query/models/mutation_job.dart';
import 'package:flutter/widgets.dart';
enum MutationStatus {
failed,
succeed,
pending,
error,
success,
loading,
idle,
}
typedef MutationListener<T> = FutureOr<void> Function(T);
typedef MutationTaskFunction<T, V> = FutureOr<T> Function(String, V);
class Mutation<T extends Object, V> extends ChangeNotifier {
class Mutation<T extends Object, V> extends BaseOperation<T, MutationStatus> {
// all params
final String mutationKey;
MutationTaskFunction<T, V> task;
final int retries;
final Duration retryDelay;
final Duration _cacheTime;
// all properties
T? data;
dynamic error;
MutationStatus status;
/// total count of how many times the query retried to get a successful
/// result
int retryAttempts = 0;
DateTime updatedAt;
/// used for keeping track of mutation activity. If the are no mounts &
/// the passed cached time is over than the mutation is removed from
/// storage/cache
Set<ValueKey<String>> _mounts = {};
@protected
final Set<MutationListener<T>> onDataListeners = {};
@@ -46,15 +30,13 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
Mutation({
required this.mutationKey,
required this.task,
required this.retries,
required this.retryDelay,
required super.retries,
required super.retryDelay,
required Duration cacheTime,
MutationListener<T>? onData,
MutationListener<dynamic>? onError,
MutationListener<V>? onMutate,
}) : status = MutationStatus.pending,
updatedAt = DateTime.now(),
_cacheTime = cacheTime {
}) : super(cacheTime: cacheTime, status: MutationStatus.idle) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
if (onMutate != null) onMutateListeners.add(onMutate);
@@ -67,61 +49,38 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
MutationListener<V>? onMutate,
}) : mutationKey = options.mutationKey,
task = options.task,
retries = options.retries ?? 3,
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200),
_cacheTime = options.cacheTime ?? const Duration(minutes: 5),
status = MutationStatus.pending,
updatedAt = DateTime.now() {
super(
retries: options.retries ?? 3,
retryDelay: options.retryDelay ?? const Duration(milliseconds: 200),
cacheTime: options.cacheTime ?? const Duration(minutes: 5),
status: MutationStatus.idle,
) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
}
// all getters & setters
bool get hasData => data != null && error == null;
bool get hasError =>
status == MutationStatus.failed && error != null && data == null;
bool get isLoading =>
status == MutationStatus.pending && data == null && error == null;
bool get isSucceeded => status == MutationStatus.succeed && data != null;
bool get isIdle => isSucceeded && error == null;
bool get isInactive => _mounts.isEmpty;
// all methods
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);
}
}
/// Calls the task function & doesn't check if there's already
/// cached data available
Future<void> _execMutation(V variables) async {
Future<void> _execute(V variables) async {
try {
status = MutationStatus.loading;
notifyListeners();
retryAttempts = 0;
for (final onMutate in onMutateListeners) {
onMutate(variables);
}
data = await task(mutationKey, variables);
updatedAt = DateTime.now();
status = MutationStatus.succeed;
status = MutationStatus.success;
for (final onData in onDataListeners) {
onData(data!);
}
notifyListeners();
} catch (e) {
if (retries == 0) {
status = MutationStatus.failed;
status = MutationStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
@@ -136,7 +95,7 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
onMutate(variables);
}
data = await task(mutationKey, variables);
status = MutationStatus.succeed;
status = MutationStatus.success;
for (final onData in onDataListeners) {
onData(data!);
}
@@ -144,7 +103,7 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
break;
} catch (e) {
if (retryAttempts == retries) {
status = MutationStatus.failed;
status = MutationStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
@@ -165,25 +124,45 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
}) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
_execMutation(variables).then((_) {
_execute(variables).then((_) {
onDataListeners.remove(onData);
onErrorListeners.remove(onError);
});
}
Future<T?> mutateAsync(V variables) async {
return await _execMutation(variables).then((_) => data);
return await _execute(variables).then((_) => data);
}
reset() {
/// 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.pending;
status = MutationStatus.idle;
onMutateListeners.clear();
}
A? cast<A>() => this is A ? this as A : null;
@override
bool get isError => status == MutationStatus.error;
@override
bool get isIdle => status == MutationStatus.idle;
@override
bool get isLoading => status == MutationStatus.loading;
@override
bool get isSuccess => status == MutationStatus.success;
}
+107 -78
View File
@@ -1,12 +1,25 @@
import 'dart:async';
import 'package:fl_query/base_operation.dart';
import 'package:fl_query/models/query_job.dart';
import 'package:flutter/widgets.dart';
enum QueryStatus {
failed,
succeed,
pending,
/// 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;
}
@@ -18,36 +31,23 @@ typedef ListenerUnsubscriber = void Function();
typedef QueryUpdateFunction<T> = FutureOr<T> Function(T? oldData);
class Query<T extends Object, Outside> extends ChangeNotifier {
class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
// all params
final String queryKey;
QueryTaskFunction<T, Outside> task;
/// The number of times the query should refetch in the time of error
/// before giving up
final int retries;
final Duration retryDelay;
bool? refetchOnMount;
final T? _initialData;
// got from global options
final Duration _staleTime;
final Duration _cacheTime;
// all properties
T? data;
dynamic error;
QueryStatus status;
Duration _staleTime;
/// total count of how many times the query retried to get a successful
/// result
int retryAttempts = 0;
DateTime updatedAt;
int refetchCount = 0;
bool enabled;
@protected
bool fetched = false;
@protected
final Set<QueryListener<T>> onDataListeners = Set<QueryListener<T>>();
@protected
@@ -60,32 +60,37 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
Outside? _prevUsedExternalData;
/// 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 = {};
Duration? refetchInterval;
Timer? _refetchIntervalTimer;
Query({
required this.queryKey,
required this.task,
required Duration staleTime,
required Duration cacheTime,
required super.cacheTime,
required Outside externalData,
required this.retries,
required this.retryDelay,
T? initialData,
required super.retries,
required super.retryDelay,
this.refetchOnMount,
this.refetchInterval,
this.enabled = true,
T? initialData,
QueryListener<T>? onData,
QueryListener<dynamic>? onError,
}) : status = QueryStatus.pending,
_staleTime = staleTime,
_cacheTime = cacheTime,
}) : _staleTime = staleTime,
_initialData = initialData,
_externalData = externalData,
data = initialData,
updatedAt = DateTime.now() {
super(
status: QueryStatus.idle,
data: initialData,
) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
if (refetchInterval != null && refetchInterval != Duration.zero) {
_refetchIntervalTimer = _createRefetchTimer();
}
}
Query.fromOptions(
@@ -96,50 +101,34 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
}) : queryKey = options.queryKey,
enabled = options.enabled ?? true,
task = options.task,
retries = options.retries ?? 3,
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200),
_staleTime = options.staleTime ?? const Duration(milliseconds: 500),
_cacheTime = options.cacheTime ?? const Duration(minutes: 5),
_initialData = options.initialData,
_externalData = externalData,
data = options.initialData,
status = QueryStatus.pending,
updatedAt = DateTime.now() {
refetchInterval = options.refetchInterval,
refetchOnMount = options.refetchOnMount,
super(
status: QueryStatus.idle,
cacheTime: options.cacheTime ?? const Duration(minutes: 5),
retries: options.retries ?? 3,
retryDelay: options.retryDelay ?? const Duration(milliseconds: 200),
data: options.initialData,
) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
}
// all getters & setters
bool get hasData => data != null && error == null;
bool get hasError =>
status == QueryStatus.failed && error != null && data == null;
bool get isLoading =>
status == QueryStatus.pending && data == null && error == null;
bool get isRefetching =>
status == QueryStatus.refetching && (data != null || error != null);
bool get isSucceeded => status == QueryStatus.succeed && data != null;
bool get isIdle => isSucceeded && error == null;
bool get isInactive => _mounts.isEmpty;
Outside get externalData => _externalData;
Outside? get prevUsedExternalData => _prevUsedExternalData;
// all methods
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);
}
Timer _createRefetchTimer() {
return Timer.periodic(
refetchInterval!,
(_) async {
if (isStale) await refetch();
},
);
}
/// Calls the task function & doesn't check if there's already
@@ -150,14 +139,14 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData;
updatedAt = DateTime.now();
status = QueryStatus.succeed;
status = QueryStatus.success;
for (final onData in onDataListeners) {
onData(data!);
}
notifyListeners();
} catch (e) {
if (retries == 0) {
status = QueryStatus.failed;
status = QueryStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
@@ -170,18 +159,18 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
try {
data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData;
status = QueryStatus.succeed;
status = QueryStatus.success;
for (final onData in onDataListeners) {
onData(data!);
await onData(data!);
}
notifyListeners();
break;
} catch (e) {
if (retryAttempts == retries) {
status = QueryStatus.failed;
status = QueryStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
await onError(error);
}
notifyListeners();
}
@@ -193,12 +182,12 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
}
Future<T?> fetch() async {
status = QueryStatus.pending;
notifyListeners();
if (!enabled) return null;
if (!isStale && hasData) {
if (hasData) {
return data;
}
status = QueryStatus.loading;
notifyListeners();
return _execute().then((_) {
fetched = true;
return data;
@@ -234,11 +223,12 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
"[fl_query] new instance of data should be returned because of immutability");
}
data = newData;
status = QueryStatus.succeed;
status = QueryStatus.success;
notifyListeners();
}
setExternalData(Outside externalData) {
_prevUsedExternalData = _externalData;
_externalData = externalData;
}
@@ -247,19 +237,58 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
data = _initialData;
error = null;
fetched = false;
status = QueryStatus.pending;
status = QueryStatus.idle;
retryAttempts = 0;
onDataListeners.clear();
onErrorListeners.clear();
_mounts.clear();
mounts.clear();
}
/// Update configurations of the query after already creating the Query
/// instance
void updateDefaultOptions({
Duration? refetchInterval,
Duration? staleTime,
Duration? cacheTime,
bool? refetchOnMount,
}) {
if (this.refetchInterval == null &&
refetchInterval != null &&
refetchInterval != Duration.zero) {
this.refetchInterval = refetchInterval;
_refetchIntervalTimer?.cancel();
_refetchIntervalTimer = _createRefetchTimer();
}
if (this.cacheTime == Duration(minutes: 5) && cacheTime != null)
this.cacheTime = cacheTime;
if (this._staleTime == const Duration(milliseconds: 500) &&
staleTime != null) this._staleTime = staleTime;
if (this.refetchOnMount == null && refetchOnMount != null)
this.refetchOnMount = refetchOnMount;
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 current DateTime is after [update_at + stale_time] it means
// the data has become stale
return DateTime.now().isAfter(updatedAt.add(_staleTime));
}
@override
bool get isError => status == QueryStatus.error;
@override
bool get isIdle => status == QueryStatus.idle;
@override
bool get isLoading => status == QueryStatus.loading;
bool get isRefetching => status == QueryStatus.refetching;
@override
bool get isSuccess => status == QueryStatus.success;
A? cast<A>() => this is A ? this as A : null;
String get debugLabel => "Query($queryKey)";
+56 -21
View File
@@ -4,6 +4,7 @@ import 'package:fl_query/models/query_job.dart';
import 'package:fl_query/mutation.dart';
import 'package:fl_query/query.dart';
import 'package:collection/collection.dart';
import 'package:fl_query/utils.dart';
import 'package:flutter/widgets.dart';
class QueryBowlScope extends StatefulWidget {
@@ -11,14 +12,30 @@ class QueryBowlScope extends StatefulWidget {
final Duration staleTime;
final Duration cacheTime;
// refetching options
// refetch query when new query instance mounts
final bool refetchOnMount;
// for desktop & web only
final bool refetchOnWindowFocus;
// for mobile only
final bool refetchOnApplicationResume;
// refetch when user's device reconnects to the internet after no being
// connected before
final bool refetchOnReconnect;
/// used for periodically checking if any query got stale.
/// If none is supplied then half of the value of staleTime is used
final Duration refreshInterval;
final Duration refetchInterval;
const QueryBowlScope({
required this.child,
this.staleTime = const Duration(milliseconds: 500),
this.staleTime = Duration.zero,
this.cacheTime = const Duration(minutes: 5),
this.refreshInterval = const Duration(minutes: 5),
this.refetchInterval = Duration.zero,
this.refetchOnMount = false,
this.refetchOnReconnect = true,
this.refetchOnApplicationResume = true,
this.refetchOnWindowFocus = true,
Key? key,
}) : super(key: key);
@@ -30,34 +47,19 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
late Set<Query> queries;
late Set<Mutation> mutations;
late Timer refreshIntervalTimer;
@override
void initState() {
super.initState();
queries = {};
mutations = {};
refreshIntervalTimer = Timer.periodic(
widget.refreshInterval,
_checkAndUpdateStaleQueriesOnBg,
);
}
@override
void dispose() {
refreshIntervalTimer.cancel();
_disposeUpdateListeners();
super.dispose();
}
Future<void> _checkAndUpdateStaleQueriesOnBg([dynamic _]) async {
// checking for staled queries inside the widget as InheritedWidget
// classes has to be constant & doesn't this kind of dynamic behavior
for (final query in queries) {
if (query.isStale) await query.refetch();
}
}
void _listenToUpdates() {
for (final query in queries) {
query.addListener(() => updateQueries(query));
@@ -147,6 +149,9 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
queries: queries,
mutations: mutations,
staleTime: widget.staleTime,
cacheTime: widget.cacheTime,
refetchInterval: widget.refetchInterval,
refetchOnMount: widget.refetchOnMount,
child: widget.child,
);
}
@@ -158,6 +163,10 @@ class QueryBowl extends InheritedWidget {
final Set<Query> _queries;
final Set<Mutation> _mutations;
final Duration staleTime;
final Duration cacheTime;
final Duration? refetchInterval;
final bool refetchOnMount;
final void Function<T extends Object, Outside>(Query<T, Outside> query)
_addQuery;
@@ -179,8 +188,11 @@ class QueryBowl extends InheritedWidget {
required final Set<Query> queries,
required final Set<Mutation> mutations,
required this.staleTime,
required this.cacheTime,
required this.removeQueries,
required this.clear,
required this.refetchOnMount,
this.refetchInterval,
Key? key,
}) : _addQuery = addQuery,
_queries = queries,
@@ -201,8 +213,12 @@ class QueryBowl extends InheritedWidget {
if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has
// changed
final hasExternalDataChanged =
prevQuery.prevUsedExternalData != externalData;
final hasExternalDataChanged = prevQuery.prevUsedExternalData != null &&
externalData != null &&
!isShallowEqual(
prevQuery.prevUsedExternalData!,
externalData,
);
prevQuery.mount(key);
if (onData != null) prevQuery.onDataListeners.add(onData);
if (onError != null) prevQuery.onErrorListeners.add(onError);
@@ -215,6 +231,12 @@ class QueryBowl extends InheritedWidget {
// mounting the widget that is using the query in the prevQuery
return prevQuery.data;
}
/// populating with default configurations
options.refetchInterval ??= refetchInterval;
options.staleTime ??= staleTime;
options.cacheTime ??= cacheTime;
options.refetchOnMount ??= refetchOnMount;
final query = Query<T, Outside>.fromOptions(
options,
externalData: externalData,
@@ -238,8 +260,14 @@ class QueryBowl extends InheritedWidget {
if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has
// changed
if (prevQuery.prevUsedExternalData != query.externalData)
if (prevQuery.prevUsedExternalData != null &&
query.externalData != null &&
!isShallowEqual(
prevQuery.prevUsedExternalData!,
query.externalData!,
)) {
prevQuery.setExternalData(query.externalData);
}
prevQuery.mount(key);
if (onData != null) prevQuery.onDataListeners.add(onData);
if (onError != null) prevQuery.onErrorListeners.add(onError);
@@ -248,6 +276,12 @@ class QueryBowl extends InheritedWidget {
}
if (onData != null) query.onDataListeners.add(onData);
if (onError != null) query.onErrorListeners.add(onError);
query.updateDefaultOptions(
cacheTime: cacheTime,
staleTime: staleTime,
refetchInterval: refetchInterval,
refetchOnMount: refetchOnMount,
);
query.mount(key);
_addQuery<T, Outside>(query);
return query;
@@ -270,6 +304,7 @@ class QueryBowl extends InheritedWidget {
prevMutation.mount(key);
return prevMutation;
} else {
mutation.updateDefaultOptions(cacheTime: cacheTime);
mutation.mount(key);
_addMutation(mutation);
return mutation;
+9 -2
View File
@@ -51,13 +51,20 @@ class _QueryBuilderState<T extends Object, Outside>
onData: widget.onData,
onError: widget.onError,
);
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();
});
}
@override
void didUpdateWidget(covariant oldWidget) {
if (oldWidget.externalData != widget.externalData) {
if (oldWidget.externalData != null &&
widget.externalData != null &&
!isShallowEqual(oldWidget.externalData!, widget.externalData!)) {
QueryBowl.of(context).fetchQuery(
widget.job,
externalData: widget.externalData,
+30
View File
@@ -11,3 +11,33 @@ Future<void> callQueryListeners<T>(Set<QueryListener<T>> listeners, T 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(Object obj1, Object obj2) {
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;
}
}
+2 -2
View File
@@ -30,12 +30,12 @@ void main() {
expect(query.retryAttempts, 0);
expect(query.fetched, isFalse);
expect(query.retryDelay, Duration(milliseconds: 200));
expect(query.status, QueryStatus.pending);
expect(query.status, QueryStatus.loading);
expect(query.isStale, isFalse);
expect(query.isIdle, isFalse);
expect(query.isInactive, isTrue);
expect(query.isLoading, isTrue);
expect(query.isSucceeded, isFalse);
expect(query.isSuccess, isFalse);
expect(query.hasError, isFalse);
expect(query.hasData, isFalse);
});