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(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -68,7 +86,7 @@ class _MyHomePageState extends State<MyHomePage> {
job: successJob, job: successJob,
externalData: null, externalData: null,
builder: (context, query) { builder: (context, query) {
if (query.isLoading || query.isRefetching) { if (!query.hasData || query.isLoading || query.isRefetching) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
return TextButton( return TextButton(
@@ -83,7 +101,7 @@ class _MyHomePageState extends State<MyHomePage> {
job: successJob, job: successJob,
externalData: null, externalData: null,
builder: (context, query) { builder: (context, query) {
if (query.isLoading || query.isRefetching) { if (!query.hasData || query.isLoading || query.isRefetching) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
return ElevatedButton( return ElevatedButton(
+1 -2
View File
@@ -1,8 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:fl_query/models/mutation_job.dart'; import 'package:fl_query/fl_query.dart';
import 'package:fl_query/mutation_builder.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -22,7 +22,7 @@ class QueryWithExternalData extends StatelessWidget {
job: jobWithExternalData, job: jobWithExternalData,
externalData: (Random().nextDouble() * 200).toString(), externalData: (Random().nextDouble() * 200).toString(),
builder: (context, query) { builder: (context, query) {
if (query.isLoading || query.isLoading || query.data == null) { if (query.isLoading || query.isRefetching || !query.hasData) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
return Container( return Container(
@@ -32,6 +32,7 @@ class QueryWithExternalData extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.blue, 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.dart';
export 'query_bowl.dart'; export 'query_bowl.dart';
export 'query_builder.dart'; export 'query_builder.dart';
export 'mutation.dart';
export 'mutation_builder.dart';
export 'models/query_job.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'; import 'package:fl_query/mutation.dart';
class MutationJob<T extends Object, V> { class MutationJob<T extends Object, V> {
final String mutationKey; String _mutationKey;
MutationTaskFunction<T, V> task; MutationTaskFunction<T, V> task;
final int? retries; final int? retries;
final Duration? retryDelay; final Duration? retryDelay;
final Duration? cacheTime; final Duration? cacheTime;
MutationJob({ MutationJob({
required this.mutationKey, required String mutationKey,
required this.task, required this.task,
this.retries, this.retries,
this.retryDelay, this.retryDelay,
this.cacheTime, 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> { class QueryJob<T extends Object, Outside> {
// all params // all params
final String queryKey; String _queryKey;
QueryTaskFunction<T, Outside> task; QueryTaskFunction<T, Outside> task;
final int? retries; final int? retries;
final Duration? retryDelay; final Duration? retryDelay;
@@ -13,11 +13,14 @@ class QueryJob<T extends Object, Outside> {
final bool? enabled; final bool? enabled;
// got from global options // got from global options
final Duration? staleTime; bool? refetchOnMount;
final Duration? cacheTime; Duration? staleTime;
Duration? cacheTime;
Duration? refetchInterval;
QueryJob({ QueryJob({
required this.queryKey, required String queryKey,
required this.task, required this.task,
this.retries, this.retries,
this.retryDelay, this.retryDelay,
@@ -25,5 +28,43 @@ class QueryJob<T extends Object, Outside> {
this.staleTime, this.staleTime,
this.cacheTime, this.cacheTime,
this.enabled, 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 'dart:async';
import 'package:fl_query/base_operation.dart';
import 'package:fl_query/models/mutation_job.dart'; import 'package:fl_query/models/mutation_job.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum MutationStatus { enum MutationStatus {
failed, error,
succeed, success,
pending, loading,
idle,
} }
typedef MutationListener<T> = FutureOr<void> Function(T); typedef MutationListener<T> = FutureOr<void> Function(T);
typedef MutationTaskFunction<T, V> = FutureOr<T> Function(String, V); 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 // all params
final String mutationKey; final String mutationKey;
MutationTaskFunction<T, V> task; 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 @protected
final Set<MutationListener<T>> onDataListeners = {}; final Set<MutationListener<T>> onDataListeners = {};
@@ -46,15 +30,13 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
Mutation({ Mutation({
required this.mutationKey, required this.mutationKey,
required this.task, required this.task,
required this.retries, required super.retries,
required this.retryDelay, required super.retryDelay,
required Duration cacheTime, required Duration cacheTime,
MutationListener<T>? onData, MutationListener<T>? onData,
MutationListener<dynamic>? onError, MutationListener<dynamic>? onError,
MutationListener<V>? onMutate, MutationListener<V>? onMutate,
}) : status = MutationStatus.pending, }) : super(cacheTime: cacheTime, status: MutationStatus.idle) {
updatedAt = DateTime.now(),
_cacheTime = cacheTime {
if (onData != null) onDataListeners.add(onData); if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError); if (onError != null) onErrorListeners.add(onError);
if (onMutate != null) onMutateListeners.add(onMutate); if (onMutate != null) onMutateListeners.add(onMutate);
@@ -67,61 +49,38 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
MutationListener<V>? onMutate, MutationListener<V>? onMutate,
}) : mutationKey = options.mutationKey, }) : mutationKey = options.mutationKey,
task = options.task, task = options.task,
retries = options.retries ?? 3, super(
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200), retries: options.retries ?? 3,
_cacheTime = options.cacheTime ?? const Duration(minutes: 5), retryDelay: options.retryDelay ?? const Duration(milliseconds: 200),
status = MutationStatus.pending, cacheTime: options.cacheTime ?? const Duration(minutes: 5),
updatedAt = DateTime.now() { status: MutationStatus.idle,
) {
if (onData != null) onDataListeners.add(onData); if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError); 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 // 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 /// Calls the task function & doesn't check if there's already
/// cached data available /// cached data available
Future<void> _execMutation(V variables) async { Future<void> _execute(V variables) async {
try { try {
status = MutationStatus.loading;
notifyListeners();
retryAttempts = 0; retryAttempts = 0;
for (final onMutate in onMutateListeners) { for (final onMutate in onMutateListeners) {
onMutate(variables); onMutate(variables);
} }
data = await task(mutationKey, variables); data = await task(mutationKey, variables);
updatedAt = DateTime.now(); updatedAt = DateTime.now();
status = MutationStatus.succeed; status = MutationStatus.success;
for (final onData in onDataListeners) { for (final onData in onDataListeners) {
onData(data!); onData(data!);
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
if (retries == 0) { if (retries == 0) {
status = MutationStatus.failed; status = MutationStatus.error;
error = e; error = e;
for (final onError in onErrorListeners) { for (final onError in onErrorListeners) {
onError(error); onError(error);
@@ -136,7 +95,7 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
onMutate(variables); onMutate(variables);
} }
data = await task(mutationKey, variables); data = await task(mutationKey, variables);
status = MutationStatus.succeed; status = MutationStatus.success;
for (final onData in onDataListeners) { for (final onData in onDataListeners) {
onData(data!); onData(data!);
} }
@@ -144,7 +103,7 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
break; break;
} catch (e) { } catch (e) {
if (retryAttempts == retries) { if (retryAttempts == retries) {
status = MutationStatus.failed; status = MutationStatus.error;
error = e; error = e;
for (final onError in onErrorListeners) { for (final onError in onErrorListeners) {
onError(error); onError(error);
@@ -165,25 +124,45 @@ class Mutation<T extends Object, V> extends ChangeNotifier {
}) { }) {
if (onData != null) onDataListeners.add(onData); if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError); if (onError != null) onErrorListeners.add(onError);
_execMutation(variables).then((_) { _execute(variables).then((_) {
onDataListeners.remove(onData); onDataListeners.remove(onData);
onErrorListeners.remove(onError); onErrorListeners.remove(onError);
}); });
} }
Future<T?> mutateAsync(V variables) async { 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; data = null;
retryAttempts = 0; retryAttempts = 0;
updatedAt = DateTime.now(); updatedAt = DateTime.now();
onDataListeners.clear(); onDataListeners.clear();
onErrorListeners.clear(); onErrorListeners.clear();
status = MutationStatus.pending; status = MutationStatus.idle;
onMutateListeners.clear(); onMutateListeners.clear();
} }
A? cast<A>() => this is A ? this as A : null; 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 'dart:async';
import 'package:fl_query/base_operation.dart';
import 'package:fl_query/models/query_job.dart'; import 'package:fl_query/models/query_job.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
enum QueryStatus { enum QueryStatus {
failed, /// in times when an error occurs
succeed, /// will get reset to idle on refetch/retry
pending, 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; refetching;
} }
@@ -18,36 +31,23 @@ typedef ListenerUnsubscriber = void Function();
typedef QueryUpdateFunction<T> = FutureOr<T> Function(T? oldData); 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 // all params
final String queryKey; final String queryKey;
QueryTaskFunction<T, Outside> task; QueryTaskFunction<T, Outside> task;
/// The number of times the query should refetch in the time of error bool? refetchOnMount;
/// before giving up
final int retries;
final Duration retryDelay;
final T? _initialData; final T? _initialData;
// got from global options // got from global options
final Duration _staleTime; Duration _staleTime;
final Duration _cacheTime;
// all properties
T? data;
dynamic error;
QueryStatus status;
/// total count of how many times the query retried to get a successful /// total count of how many times the query retried to get a successful
/// result /// result
int retryAttempts = 0;
DateTime updatedAt;
int refetchCount = 0; int refetchCount = 0;
bool enabled; bool enabled;
@protected
bool fetched = false;
@protected @protected
final Set<QueryListener<T>> onDataListeners = Set<QueryListener<T>>(); final Set<QueryListener<T>> onDataListeners = Set<QueryListener<T>>();
@protected @protected
@@ -60,32 +60,37 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
Outside? _prevUsedExternalData; Outside? _prevUsedExternalData;
/// used for keeping track of query activity. If the are no mounts & Duration? refetchInterval;
/// the passed cached time is over than the query is removed from
/// storage/cache Timer? _refetchIntervalTimer;
Set<ValueKey<String>> _mounts = {};
Query({ Query({
required this.queryKey, required this.queryKey,
required this.task, required this.task,
required Duration staleTime, required Duration staleTime,
required Duration cacheTime, required super.cacheTime,
required Outside externalData, required Outside externalData,
required this.retries, required super.retries,
required this.retryDelay, required super.retryDelay,
T? initialData, this.refetchOnMount,
this.refetchInterval,
this.enabled = true, this.enabled = true,
T? initialData,
QueryListener<T>? onData, QueryListener<T>? onData,
QueryListener<dynamic>? onError, QueryListener<dynamic>? onError,
}) : status = QueryStatus.pending, }) : _staleTime = staleTime,
_staleTime = staleTime,
_cacheTime = cacheTime,
_initialData = initialData, _initialData = initialData,
_externalData = externalData, _externalData = externalData,
data = initialData, super(
updatedAt = DateTime.now() { status: QueryStatus.idle,
data: initialData,
) {
if (onData != null) onDataListeners.add(onData); if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError); if (onError != null) onErrorListeners.add(onError);
if (refetchInterval != null && refetchInterval != Duration.zero) {
_refetchIntervalTimer = _createRefetchTimer();
}
} }
Query.fromOptions( Query.fromOptions(
@@ -96,50 +101,34 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
}) : queryKey = options.queryKey, }) : queryKey = options.queryKey,
enabled = options.enabled ?? true, enabled = options.enabled ?? true,
task = options.task, task = options.task,
retries = options.retries ?? 3,
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200),
_staleTime = options.staleTime ?? const Duration(milliseconds: 500), _staleTime = options.staleTime ?? const Duration(milliseconds: 500),
_cacheTime = options.cacheTime ?? const Duration(minutes: 5),
_initialData = options.initialData, _initialData = options.initialData,
_externalData = externalData, _externalData = externalData,
data = options.initialData, refetchInterval = options.refetchInterval,
status = QueryStatus.pending, refetchOnMount = options.refetchOnMount,
updatedAt = DateTime.now() { 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 (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError); if (onError != null) onErrorListeners.add(onError);
} }
// all getters & setters // 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 externalData => _externalData;
Outside? get prevUsedExternalData => _prevUsedExternalData; Outside? get prevUsedExternalData => _prevUsedExternalData;
// all methods Timer _createRefetchTimer() {
return Timer.periodic(
void mount(ValueKey<String> uKey) { refetchInterval!,
_mounts.add(uKey); (_) async {
} if (isStale) await refetch();
},
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 /// 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); data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData; _prevUsedExternalData = _externalData;
updatedAt = DateTime.now(); updatedAt = DateTime.now();
status = QueryStatus.succeed; status = QueryStatus.success;
for (final onData in onDataListeners) { for (final onData in onDataListeners) {
onData(data!); onData(data!);
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
if (retries == 0) { if (retries == 0) {
status = QueryStatus.failed; status = QueryStatus.error;
error = e; error = e;
for (final onError in onErrorListeners) { for (final onError in onErrorListeners) {
onError(error); onError(error);
@@ -170,18 +159,18 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
try { try {
data = await task(queryKey, _externalData); data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData; _prevUsedExternalData = _externalData;
status = QueryStatus.succeed; status = QueryStatus.success;
for (final onData in onDataListeners) { for (final onData in onDataListeners) {
onData(data!); await onData(data!);
} }
notifyListeners(); notifyListeners();
break; break;
} catch (e) { } catch (e) {
if (retryAttempts == retries) { if (retryAttempts == retries) {
status = QueryStatus.failed; status = QueryStatus.error;
error = e; error = e;
for (final onError in onErrorListeners) { for (final onError in onErrorListeners) {
onError(error); await onError(error);
} }
notifyListeners(); notifyListeners();
} }
@@ -193,12 +182,12 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
} }
Future<T?> fetch() async { Future<T?> fetch() async {
status = QueryStatus.pending;
notifyListeners();
if (!enabled) return null; if (!enabled) return null;
if (!isStale && hasData) { if (hasData) {
return data; return data;
} }
status = QueryStatus.loading;
notifyListeners();
return _execute().then((_) { return _execute().then((_) {
fetched = true; fetched = true;
return data; 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"); "[fl_query] new instance of data should be returned because of immutability");
} }
data = newData; data = newData;
status = QueryStatus.succeed; status = QueryStatus.success;
notifyListeners(); notifyListeners();
} }
setExternalData(Outside externalData) { setExternalData(Outside externalData) {
_prevUsedExternalData = _externalData;
_externalData = externalData; _externalData = externalData;
} }
@@ -247,19 +237,58 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
data = _initialData; data = _initialData;
error = null; error = null;
fetched = false; fetched = false;
status = QueryStatus.pending; status = QueryStatus.idle;
retryAttempts = 0; retryAttempts = 0;
onDataListeners.clear(); onDataListeners.clear();
onErrorListeners.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 { 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 // when current DateTime is after [update_at + stale_time] it means
// the data has become stale // the data has become stale
return DateTime.now().isAfter(updatedAt.add(_staleTime)); 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; A? cast<A>() => this is A ? this as A : null;
String get debugLabel => "Query($queryKey)"; 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/mutation.dart';
import 'package:fl_query/query.dart'; import 'package:fl_query/query.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_query/utils.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class QueryBowlScope extends StatefulWidget { class QueryBowlScope extends StatefulWidget {
@@ -11,14 +12,30 @@ class QueryBowlScope extends StatefulWidget {
final Duration staleTime; final Duration staleTime;
final Duration cacheTime; 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. /// used for periodically checking if any query got stale.
/// If none is supplied then half of the value of staleTime is used /// If none is supplied then half of the value of staleTime is used
final Duration refreshInterval; final Duration refetchInterval;
const QueryBowlScope({ const QueryBowlScope({
required this.child, required this.child,
this.staleTime = const Duration(milliseconds: 500), this.staleTime = Duration.zero,
this.cacheTime = const Duration(minutes: 5), 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, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -30,34 +47,19 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
late Set<Query> queries; late Set<Query> queries;
late Set<Mutation> mutations; late Set<Mutation> mutations;
late Timer refreshIntervalTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
queries = {}; queries = {};
mutations = {}; mutations = {};
refreshIntervalTimer = Timer.periodic(
widget.refreshInterval,
_checkAndUpdateStaleQueriesOnBg,
);
} }
@override @override
void dispose() { void dispose() {
refreshIntervalTimer.cancel();
_disposeUpdateListeners(); _disposeUpdateListeners();
super.dispose(); 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() { void _listenToUpdates() {
for (final query in queries) { for (final query in queries) {
query.addListener(() => updateQueries(query)); query.addListener(() => updateQueries(query));
@@ -147,6 +149,9 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
queries: queries, queries: queries,
mutations: mutations, mutations: mutations,
staleTime: widget.staleTime, staleTime: widget.staleTime,
cacheTime: widget.cacheTime,
refetchInterval: widget.refetchInterval,
refetchOnMount: widget.refetchOnMount,
child: widget.child, child: widget.child,
); );
} }
@@ -158,6 +163,10 @@ class QueryBowl extends InheritedWidget {
final Set<Query> _queries; final Set<Query> _queries;
final Set<Mutation> _mutations; final Set<Mutation> _mutations;
final Duration staleTime; final Duration staleTime;
final Duration cacheTime;
final Duration? refetchInterval;
final bool refetchOnMount;
final void Function<T extends Object, Outside>(Query<T, Outside> query) final void Function<T extends Object, Outside>(Query<T, Outside> query)
_addQuery; _addQuery;
@@ -179,8 +188,11 @@ class QueryBowl extends InheritedWidget {
required final Set<Query> queries, required final Set<Query> queries,
required final Set<Mutation> mutations, required final Set<Mutation> mutations,
required this.staleTime, required this.staleTime,
required this.cacheTime,
required this.removeQueries, required this.removeQueries,
required this.clear, required this.clear,
required this.refetchOnMount,
this.refetchInterval,
Key? key, Key? key,
}) : _addQuery = addQuery, }) : _addQuery = addQuery,
_queries = queries, _queries = queries,
@@ -201,8 +213,12 @@ class QueryBowl extends InheritedWidget {
if (prevQuery is Query<T, Outside>) { if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has // run the query if its still not called or if externalData has
// changed // changed
final hasExternalDataChanged = final hasExternalDataChanged = prevQuery.prevUsedExternalData != null &&
prevQuery.prevUsedExternalData != externalData; externalData != null &&
!isShallowEqual(
prevQuery.prevUsedExternalData!,
externalData,
);
prevQuery.mount(key); prevQuery.mount(key);
if (onData != null) prevQuery.onDataListeners.add(onData); if (onData != null) prevQuery.onDataListeners.add(onData);
if (onError != null) prevQuery.onErrorListeners.add(onError); 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 // mounting the widget that is using the query in the prevQuery
return prevQuery.data; 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( final query = Query<T, Outside>.fromOptions(
options, options,
externalData: externalData, externalData: externalData,
@@ -238,8 +260,14 @@ class QueryBowl extends InheritedWidget {
if (prevQuery is Query<T, Outside>) { if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has // run the query if its still not called or if externalData has
// changed // changed
if (prevQuery.prevUsedExternalData != query.externalData) if (prevQuery.prevUsedExternalData != null &&
query.externalData != null &&
!isShallowEqual(
prevQuery.prevUsedExternalData!,
query.externalData!,
)) {
prevQuery.setExternalData(query.externalData); prevQuery.setExternalData(query.externalData);
}
prevQuery.mount(key); prevQuery.mount(key);
if (onData != null) prevQuery.onDataListeners.add(onData); if (onData != null) prevQuery.onDataListeners.add(onData);
if (onError != null) prevQuery.onErrorListeners.add(onError); if (onError != null) prevQuery.onErrorListeners.add(onError);
@@ -248,6 +276,12 @@ class QueryBowl extends InheritedWidget {
} }
if (onData != null) query.onDataListeners.add(onData); if (onData != null) query.onDataListeners.add(onData);
if (onError != null) query.onErrorListeners.add(onError); if (onError != null) query.onErrorListeners.add(onError);
query.updateDefaultOptions(
cacheTime: cacheTime,
staleTime: staleTime,
refetchInterval: refetchInterval,
refetchOnMount: refetchOnMount,
);
query.mount(key); query.mount(key);
_addQuery<T, Outside>(query); _addQuery<T, Outside>(query);
return query; return query;
@@ -270,6 +304,7 @@ class QueryBowl extends InheritedWidget {
prevMutation.mount(key); prevMutation.mount(key);
return prevMutation; return prevMutation;
} else { } else {
mutation.updateDefaultOptions(cacheTime: cacheTime);
mutation.mount(key); mutation.mount(key);
_addMutation(mutation); _addMutation(mutation);
return mutation; return mutation;
+9 -2
View File
@@ -51,13 +51,20 @@ class _QueryBuilderState<T extends Object, Outside>
onData: widget.onData, onData: widget.onData,
onError: widget.onError, 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 @override
void didUpdateWidget(covariant oldWidget) { 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( QueryBowl.of(context).fetchQuery(
widget.job, widget.job,
externalData: widget.externalData, externalData: widget.externalData,
+30
View File
@@ -11,3 +11,33 @@ Future<void> callQueryListeners<T>(Set<QueryListener<T>> listeners, T data) {
} }
const uuid = Uuid(); 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.retryAttempts, 0);
expect(query.fetched, isFalse); expect(query.fetched, isFalse);
expect(query.retryDelay, Duration(milliseconds: 200)); expect(query.retryDelay, Duration(milliseconds: 200));
expect(query.status, QueryStatus.pending); expect(query.status, QueryStatus.loading);
expect(query.isStale, isFalse); expect(query.isStale, isFalse);
expect(query.isIdle, isFalse); expect(query.isIdle, isFalse);
expect(query.isInactive, isTrue); expect(query.isInactive, isTrue);
expect(query.isLoading, isTrue); expect(query.isLoading, isTrue);
expect(query.isSucceeded, isFalse); expect(query.isSuccess, isFalse);
expect(query.hasError, isFalse); expect(query.hasError, isFalse);
expect(query.hasData, isFalse); expect(query.hasData, isFalse);
}); });