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:
@@ -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,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),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user