files moved to src folder

added flutter_hooks support with example
This commit is contained in:
Kingkor Roy Tirtho
2022-06-22 13:46:40 +06:00
parent e0c70d153f
commit 8c53f1b0fe
20 changed files with 319 additions and 30 deletions
@@ -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;
}
@@ -0,0 +1,116 @@
import 'package:fl_query/src/models/mutation_job.dart';
import 'package:fl_query/src/mutation.dart';
import 'package:fl_query/src/query_bowl.dart';
import 'package:fl_query/src/utils.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Mutation<T, V> useMutation<T extends Object, V>({
required MutationJob<T, V> job,
/// Called when the query returns new data, on query
/// refetch or query gets expired
MutationListener<T>? onData,
/// Called when the query returns error
MutationListener<dynamic>? onError,
/// called right before the mutation is about to run
///
/// perfect scenario for doing optimistic updates
MutationListener<V>? onMutate,
List<Object?>? keys,
}) {
return use(_UseMutation<T, V>(
job: job,
onData: onData,
onError: onError,
onMutate: onMutate,
keys: keys,
));
}
class _UseMutation<T extends Object, V> extends Hook<Mutation<T, V>> {
final MutationJob<T, V> job;
/// Called when the query returns new data, on query
/// refetch or query gets expired
final MutationListener<T>? onData;
/// Called when the query returns error
final MutationListener<dynamic>? onError;
/// called right before the mutation is about to run
///
/// perfect scenario for doing optimistic updates
final MutationListener<V>? onMutate;
const _UseMutation({
required this.job,
this.onData,
this.onError,
this.onMutate,
super.keys,
});
@override
HookState<Mutation<T, V>, Hook<Mutation<T, V>>> createState() =>
_UseMutationHookState();
}
class _UseMutationHookState<T extends Object, V>
extends HookState<Mutation<T, V>, _UseMutation<T, V>> {
late QueryBowl queryBowl;
late final ValueKey<String> uKey;
late Mutation<T, V> mutation;
@override
void initHook() {
super.initHook();
uKey = ValueKey<String>(uuid.v4());
mutation = Mutation<T, V>.fromOptions(hook.job);
WidgetsBinding.instance.addPostFrameCallback((_) {
queryBowl = QueryBowl.of(context);
mutation = queryBowl.addMutation<T, V>(
mutation,
onData: hook.onData,
onError: hook.onError,
onMutate: hook.onMutate,
key: uKey,
);
});
}
@override
void didUpdateHook(_UseMutation<T, V> oldHook) {
if (oldHook.onData != hook.onData && oldHook.onData != null) {
mutation.onDataListeners.remove(oldHook.onData);
if (hook.onData != null) mutation.onDataListeners.add(hook.onData!);
}
if (oldHook.onError != hook.onError && oldHook.onError != null) {
mutation.onErrorListeners.remove(oldHook.onError);
if (hook.onError != null) mutation.onErrorListeners.add(hook.onError!);
}
if (oldHook.onMutate != hook.onMutate && oldHook.onMutate != null) {
mutation.onMutateListeners.remove(oldHook.onMutate);
if (hook.onMutate != null) mutation.onMutateListeners.add(hook.onMutate!);
}
super.didUpdateHook(oldHook);
}
@override
void dispose() {
mutation.unmount(uKey);
if (hook.onData != null) mutation.onDataListeners.remove(hook.onData);
if (hook.onError != null) mutation.onErrorListeners.remove(hook.onError);
if (hook.onMutate != null) mutation.onMutateListeners.remove(hook.onMutate);
}
@override
Mutation<T, V> build(BuildContext context) {
queryBowl = QueryBowl.of(context);
return queryBowl.getMutation<T, V>(mutation.mutationKey) ?? mutation;
}
@override
String get debugLabel => 'useQuery';
}
@@ -0,0 +1,122 @@
import 'package:fl_query/src/models/query_job.dart';
import 'package:fl_query/src/query.dart';
import 'package:fl_query/src/query_bowl.dart';
import 'package:fl_query/src/utils.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
Query<T, Outside> useQuery<T extends Object, Outside>({
required QueryJob<T, Outside> job,
required Outside externalData,
/// Called when the query returns new data, on query
/// refetch or query gets expired
QueryListener<T>? onData,
/// Called when the query returns error
QueryListener<dynamic>? onError,
List<Object?>? keys,
}) {
return use(_UseQuery<T, Outside>(
externalData: externalData,
job: job,
onData: onData,
onError: onError,
keys: keys,
));
}
class _UseQuery<T extends Object, Outside> extends Hook<Query<T, Outside>> {
final QueryJob<T, Outside> job;
final Outside externalData;
/// Called when the query returns new data, on query
/// refetch or query gets expired
final QueryListener<T>? onData;
/// Called when the query returns error
final QueryListener<dynamic>? onError;
const _UseQuery({
required this.job,
required this.externalData,
this.onData,
this.onError,
super.keys,
});
@override
HookState<Query<T, Outside>, Hook<Query<T, Outside>>> createState() =>
_UseQueryHookState();
}
class _UseQueryHookState<T extends Object, Outside>
extends HookState<Query<T, Outside>, _UseQuery<T, Outside>> {
late QueryBowl queryBowl;
late final ValueKey<String> uKey;
late Query<T, Outside> query;
@override
void initHook() {
super.initHook();
uKey = ValueKey<String>(uuid.v4());
query = Query<T, Outside>.fromOptions(
hook.job,
externalData: hook.externalData,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
query = QueryBowl.of(context).addQuery<T, Outside>(
query,
key: uKey,
onData: hook.onData,
onError: hook.onError,
);
final hasExternalDataChanged = query.externalData != null &&
query.prevUsedExternalData != null &&
!isShallowEqual(query.externalData!, query.prevUsedExternalData!);
(query.fetched && query.refetchOnMount == true) || hasExternalDataChanged
? await query.refetch()
: await query.fetch();
});
}
@override
void didUpdateHook(_UseQuery<T, Outside> oldHook) {
if (oldHook.externalData != null &&
hook.externalData != null &&
!isShallowEqual(oldHook.externalData!, hook.externalData!)) {
QueryBowl.of(context).fetchQuery(
hook.job,
externalData: hook.externalData,
onData: hook.onData,
onError: hook.onError,
key: uKey,
);
} else {
if (oldHook.onData != hook.onData && oldHook.onData != null) {
query.onDataListeners.remove(oldHook.onData);
if (hook.onData != null) query.onDataListeners.add(hook.onData!);
}
if (oldHook.onError != hook.onError && oldHook.onError != null) {
query.onErrorListeners.remove(oldHook.onError);
if (hook.onError != null) query.onErrorListeners.add(hook.onError!);
}
}
super.didUpdateHook(oldHook);
}
@override
void dispose() {
query.unmount(uKey);
if (hook.onData != null) query.onDataListeners.remove(hook.onData);
if (hook.onError != null) query.onErrorListeners.remove(hook.onError);
}
@override
Query<T, Outside> build(BuildContext context) {
queryBowl = QueryBowl.of(context);
return queryBowl.getQuery<T, Outside>(query.queryKey) ?? query;
}
@override
String get debugLabel => 'useQuery';
}
@@ -0,0 +1,43 @@
import 'package:fl_query/fl_query.dart';
class MutationJob<T extends Object, V> {
String _mutationKey;
MutationTaskFunction<T, V> task;
final int? retries;
final Duration? retryDelay;
final Duration? cacheTime;
MutationJob({
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,
);
};
}
}
@@ -0,0 +1,70 @@
import 'package:fl_query/src/query.dart';
class QueryJob<T extends Object, Outside> {
// all params
String _queryKey;
QueryTaskFunction<T, Outside> task;
final int? retries;
final Duration? retryDelay;
final T? initialData;
/// If set to false then the initial fetch will not be called & to
/// start the process the user has to call the refetch first
final bool? enabled;
// got from global options
bool? refetchOnMount;
Duration? staleTime;
Duration? cacheTime;
Duration? refetchInterval;
QueryJob({
required String queryKey,
required this.task,
this.retries,
this.retryDelay,
this.initialData,
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,
);
};
}
}
+168
View File
@@ -0,0 +1,168 @@
import 'dart:async';
import 'package:fl_query/src/base_operation.dart';
import 'package:fl_query/src/models/mutation_job.dart';
import 'package:flutter/widgets.dart';
enum MutationStatus {
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 BaseOperation<T, MutationStatus> {
// all params
final String mutationKey;
MutationTaskFunction<T, V> task;
@protected
final Set<MutationListener<T>> onDataListeners = {};
@protected
final Set<MutationListener<dynamic>> onErrorListeners = {};
@protected
final Set<MutationListener<V>> onMutateListeners = {};
Mutation({
required this.mutationKey,
required this.task,
required super.retries,
required super.retryDelay,
required Duration cacheTime,
MutationListener<T>? onData,
MutationListener<dynamic>? onError,
MutationListener<V>? onMutate,
}) : super(cacheTime: cacheTime, status: MutationStatus.idle) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
if (onMutate != null) onMutateListeners.add(onMutate);
}
Mutation.fromOptions(
MutationJob<T, V> options, {
MutationListener<T>? onData,
MutationListener<dynamic>? onError,
MutationListener<V>? onMutate,
}) : mutationKey = options.mutationKey,
task = options.task,
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 methods
/// Calls the task function & doesn't check if there's already
/// cached data available
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.success;
for (final onData in onDataListeners) {
onData(data!);
}
notifyListeners();
} catch (e) {
if (retries == 0) {
status = MutationStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
}
notifyListeners();
} else {
// retrying for retry count if failed for the first time
while (retryAttempts <= retries) {
await Future.delayed(retryDelay);
try {
for (final onMutate in onMutateListeners) {
onMutate(variables);
}
data = await task(mutationKey, variables);
status = MutationStatus.success;
for (final onData in onDataListeners) {
onData(data!);
}
notifyListeners();
break;
} catch (e) {
if (retryAttempts == retries) {
status = MutationStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
}
notifyListeners();
}
retryAttempts++;
}
}
}
}
}
void mutate(
V variables, {
MutationListener<T>? onData,
MutationListener<dynamic>? onError,
}) {
if (onData != null) onDataListeners.add(onData);
if (onError != null) onErrorListeners.add(onError);
_execute(variables).then((_) {
onDataListeners.remove(onData);
onErrorListeners.remove(onError);
});
}
Future<T?> mutateAsync(V variables) async {
return await _execute(variables).then((_) => data);
}
/// 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.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;
}
@@ -0,0 +1,98 @@
import 'package:fl_query/src/models/mutation_job.dart';
import 'package:fl_query/src/mutation.dart';
import 'package:fl_query/src/query_bowl.dart';
import 'package:fl_query/src/utils.dart';
import 'package:flutter/widgets.dart';
class MutationBuilder<T extends Object, V> extends StatefulWidget {
final Function(BuildContext, Mutation<T, V>) builder;
final MutationJob<T, V> job;
/// Called when the query returns new data, on query
/// refetch or query gets expired
final MutationListener<T>? onData;
/// Called when the query returns error
final MutationListener<dynamic>? onError;
/// called right before the mutation is about to run
///
/// perfect scenario for doing optimistic updates
final MutationListener<V>? onMutate;
const MutationBuilder({
required this.job,
required this.builder,
this.onData,
this.onError,
this.onMutate,
Key? key,
}) : super(key: key);
@override
State<MutationBuilder<T, V>> createState() => _MutationBuilderState<T, V>();
}
class _MutationBuilderState<T extends Object, V>
extends State<MutationBuilder<T, V>> {
late QueryBowl queryBowl;
late ValueKey<String> uKey;
late Mutation<T, V> mutation;
@override
void initState() {
super.initState();
uKey = ValueKey<String>(uuid.v4());
mutation = Mutation<T, V>.fromOptions(widget.job);
WidgetsBinding.instance.addPostFrameCallback((_) {
queryBowl = QueryBowl.of(context);
mutation = queryBowl.addMutation<T, V>(
mutation,
onData: widget.onData,
onError: widget.onError,
onMutate: widget.onMutate,
key: uKey,
);
});
}
@override
void didUpdateWidget(covariant MutationBuilder<T, V> oldWidget) {
if (oldWidget.onData != widget.onData && oldWidget.onData != null) {
mutation.onDataListeners.remove(oldWidget.onData);
if (widget.onData != null) mutation.onDataListeners.add(widget.onData!);
}
if (oldWidget.onError != widget.onError && oldWidget.onError != null) {
mutation.onErrorListeners.remove(oldWidget.onError);
if (widget.onError != null)
mutation.onErrorListeners.add(widget.onError!);
}
if (oldWidget.onMutate != widget.onMutate && oldWidget.onMutate != null) {
mutation.onMutateListeners.remove(oldWidget.onMutate);
if (widget.onMutate != null)
mutation.onMutateListeners.add(widget.onMutate!);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
mutation.unmount(uKey);
if (widget.onData != null) mutation.onDataListeners.remove(widget.onData);
if (widget.onError != null)
mutation.onErrorListeners.remove(widget.onError);
if (widget.onMutate != null)
mutation.onMutateListeners.remove(widget.onMutate);
super.dispose();
}
@override
Widget build(BuildContext context) {
queryBowl = QueryBowl.of(context);
final latestMutation =
queryBowl.getMutation<T, V>(mutation.mutationKey) ?? mutation;
return widget.builder(context, latestMutation);
}
}
+300
View File
@@ -0,0 +1,300 @@
import 'dart:async';
import 'package:fl_query/src/base_operation.dart';
import 'package:fl_query/src/models/query_job.dart';
import 'package:flutter/widgets.dart';
enum QueryStatus {
/// 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;
}
typedef QueryTaskFunction<T, Outside> = FutureOr<T> Function(String, Outside);
typedef QueryListener<T> = FutureOr<void> Function(T);
typedef ListenerUnsubscriber = void Function();
typedef QueryUpdateFunction<T> = FutureOr<T> Function(T? oldData);
class Query<T extends Object, Outside> extends BaseOperation<T, QueryStatus> {
// all params
final String queryKey;
QueryTaskFunction<T, Outside> task;
bool? refetchOnMount;
final T? _initialData;
// got from global options
Duration _staleTime;
/// total count of how many times the query retried to get a successful
/// result
int refetchCount = 0;
bool enabled;
@protected
final Set<QueryListener<T>> onDataListeners = Set<QueryListener<T>>();
@protected
final Set<QueryListener<dynamic>> onErrorListeners =
Set<QueryListener<dynamic>>();
// externalData will always be passed to the task Callback
// it will change based on the presence of QueryBuilder
Outside _externalData;
Outside? _prevUsedExternalData;
Duration? refetchInterval;
Timer? _refetchIntervalTimer;
Query({
required this.queryKey,
required this.task,
required Duration staleTime,
required super.cacheTime,
required Outside externalData,
required super.retries,
required super.retryDelay,
this.refetchOnMount,
this.refetchInterval,
this.enabled = true,
T? initialData,
QueryListener<T>? onData,
QueryListener<dynamic>? onError,
}) : _staleTime = staleTime,
_initialData = initialData,
_externalData = externalData,
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(
QueryJob<T, Outside> options, {
required Outside externalData,
QueryListener<T>? onData,
QueryListener<dynamic>? onError,
}) : queryKey = options.queryKey,
enabled = options.enabled ?? true,
task = options.task,
_staleTime = options.staleTime ?? const Duration(milliseconds: 500),
_initialData = options.initialData,
_externalData = externalData,
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
Outside get externalData => _externalData;
Outside? get prevUsedExternalData => _prevUsedExternalData;
Timer _createRefetchTimer() {
return Timer.periodic(
refetchInterval!,
(_) async {
if (isStale) await refetch();
},
);
}
/// Calls the task function & doesn't check if there's already
/// cached data available
Future<void> _execute() async {
try {
retryAttempts = 0;
data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData;
updatedAt = DateTime.now();
status = QueryStatus.success;
for (final onData in onDataListeners) {
onData(data!);
}
notifyListeners();
} catch (e) {
if (retries == 0) {
status = QueryStatus.error;
error = e;
for (final onError in onErrorListeners) {
onError(error);
}
notifyListeners();
} else {
// retrying for retry count if failed for the first time
while (retryAttempts <= retries) {
await Future.delayed(retryDelay);
try {
data = await task(queryKey, _externalData);
_prevUsedExternalData = _externalData;
status = QueryStatus.success;
for (final onData in onDataListeners) {
await onData(data!);
}
notifyListeners();
break;
} catch (e) {
if (retryAttempts == retries) {
status = QueryStatus.error;
error = e;
for (final onError in onErrorListeners) {
await onError(error);
}
notifyListeners();
}
retryAttempts++;
}
}
}
}
}
Future<T?> fetch() async {
if (!enabled) return null;
if (hasData) {
return data;
}
status = QueryStatus.loading;
notifyListeners();
return _execute().then((_) {
fetched = true;
return data;
});
}
Future<T?> refetch() async {
// cannot let run multiple refetch at the same time. It can cause
// race-condition
if (isRefetching) return null;
status = QueryStatus.refetching;
refetchCount++;
// disabling the lazy query bound when query was actually called
if (!enabled) enabled = false;
notifyListeners();
return await _execute().then((_) => data);
}
/// can be used to update the data manually. Can be useful when used
/// together with mutations to perform optimistic updates or manual data
/// updates
/// For updating particular queries after a mutation using the
/// `QueryBowl.refetchQueries` is more appropriate. But this one can be
/// used when only 1 query needs get updated
///
/// Every time a new instance of data should be returned because of
/// immutability
void setQueryData(QueryUpdateFunction<T> updateFn) async {
final newData = await updateFn(data);
if (data == newData) {
// TODO: Better Error handling & Error structure
throw Exception(
"[fl_query] new instance of data should be returned because of immutability");
}
data = newData;
status = QueryStatus.success;
notifyListeners();
}
setExternalData(Outside externalData) {
_prevUsedExternalData = _externalData;
_externalData = externalData;
}
void reset() {
refetchCount = 0;
data = _initialData;
error = null;
fetched = false;
status = QueryStatus.idle;
retryAttempts = 0;
onDataListeners.clear();
onErrorListeners.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)";
@override
String toString() {
return debugLabel;
}
}
+381
View File
@@ -0,0 +1,381 @@
import 'dart:async';
import 'package:fl_query/src/models/query_job.dart';
import 'package:fl_query/src/mutation.dart';
import 'package:fl_query/src/query.dart';
import 'package:fl_query/src/utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
class QueryBowlScope extends StatefulWidget {
final Widget child;
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 refetchInterval;
const QueryBowlScope({
required this.child,
this.staleTime = Duration.zero,
this.cacheTime = 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);
@override
State<QueryBowlScope> createState() => _QueryBowlScopeState();
}
class _QueryBowlScopeState extends State<QueryBowlScope> {
late Set<Query> queries;
late Set<Mutation> mutations;
@override
void initState() {
super.initState();
queries = {};
mutations = {};
}
@override
void dispose() {
_disposeUpdateListeners();
super.dispose();
}
void _listenToUpdates() {
for (final query in queries) {
query.addListener(() => updateQueries(query));
}
for (final mutation in mutations) {
mutation.addListener(() => updateMutations(mutation));
}
}
void _disposeUpdateListeners() {
for (final query in queries) {
query.removeListener(() => updateQueries(query));
}
for (final mutation in mutations) {
mutation.removeListener(() => updateMutations(mutation));
}
}
void updateQueries(Query query) {
setState(() {
// checking & not including inactive queries
// basically garbage collecting queries
queries = Set.from(
query.isInactive
? queries.where((el) => el.queryKey != query.queryKey)
: queries,
);
});
}
void updateMutations(Mutation mutation) {
setState(() {
// checking & not including inactive mutations
// basically garbage collecting mutations
mutations = Set.from(
mutation.isInactive
? mutations.where(
(el) => el.mutationKey != mutation.mutationKey,
)
: mutations,
);
});
}
void addQuery<T extends Object, Outside>(Query<T, Outside> query) {
setState(() {
queries = Set.from({...queries, query});
});
}
void addMutation<T extends Object, V>(Mutation<T, V> mutation) {
setState(() {
mutations = Set.from({...mutations, mutation});
});
}
int removeQueries(List<String> queryKeys) {
int count = 0;
setState(() {
mutations = Set.from(
queries.whereNot((query) {
final isAboutToRip = queryKeys.contains(query.queryKey);
if (isAboutToRip) count++;
return isAboutToRip;
}),
);
});
return count;
}
void clear() {
setState(() {
queries = Set<Query>();
mutations = Set<Mutation>();
});
}
@override
Widget build(BuildContext context) {
_disposeUpdateListeners();
_listenToUpdates();
return QueryBowl(
addQuery: addQuery,
addMutation: addMutation,
removeQueries: removeQueries,
clear: clear,
queries: queries,
mutations: mutations,
staleTime: widget.staleTime,
cacheTime: widget.cacheTime,
refetchInterval: widget.refetchInterval,
refetchOnMount: widget.refetchOnMount,
child: widget.child,
);
}
}
/// QueryBowl holds all the query related methods & properties.
/// Its responsible for creating/updating/delete queries
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;
final void Function<T extends Object, V>(Mutation<T, V> mutation)
_addMutation;
final int Function(List<String>) removeQueries;
final void Function() clear;
const QueryBowl({
required Widget child,
required final void Function<T extends Object, Outside>(
Query<T, Outside> query)
addQuery,
required final void Function<T extends Object, V>(Mutation<T, V> mutation)
addMutation,
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,
_mutations = mutations,
_addMutation = addMutation,
super(child: child, key: key);
@protected
Future<T?> fetchQuery<T extends Object, Outside>(
QueryJob<T, Outside> options, {
required Outside externalData,
final QueryListener<T>? onData,
final QueryListener<dynamic>? onError,
required ValueKey<String> key,
}) async {
final prevQuery =
_queries.firstWhereOrNull((q) => q.queryKey == options.queryKey);
if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has
// changed
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);
if (!prevQuery.hasData || hasExternalDataChanged) {
if (hasExternalDataChanged) prevQuery.setExternalData(externalData);
return prevQuery.fetched
? await prevQuery.refetch()
: await prevQuery.fetch();
}
// 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,
onData: onData,
onError: onError,
);
query.mount(key);
_addQuery<T, Outside>(query);
return await query.fetch();
}
@protected
Query<T, Outside> addQuery<T extends Object, Outside>(
Query<T, Outside> query, {
required ValueKey<String> key,
final QueryListener<T>? onData,
final QueryListener<dynamic>? onError,
}) {
final prevQuery =
_queries.firstWhereOrNull((q) => q.queryKey == query.queryKey);
if (prevQuery is Query<T, Outside>) {
// run the query if its still not called or if externalData has
// changed
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);
// mounting the widget that is using the query in the prevQuery
return prevQuery;
}
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;
}
@protected
Mutation<T, V> addMutation<T extends Object, V>(
Mutation<T, V> mutation, {
final MutationListener<T>? onData,
final MutationListener<dynamic>? onError,
final MutationListener<V>? onMutate,
required ValueKey<String> key,
}) {
final prevMutation = _mutations.firstWhereOrNull(
(prevMutation) => prevMutation.mutationKey == mutation.mutationKey);
if (prevMutation != null && prevMutation is Mutation<T, V>) {
if (onData != null) prevMutation.onDataListeners.add(onData);
if (onError != null) prevMutation.onErrorListeners.add(onError);
if (onMutate != null) prevMutation.onMutateListeners.add(onMutate);
prevMutation.mount(key);
return prevMutation;
} else {
mutation.updateDefaultOptions(cacheTime: cacheTime);
mutation.mount(key);
_addMutation(mutation);
return mutation;
}
}
Query<T, Outside>? getQuery<T extends Object, Outside>(String queryKey) {
return _queries.firstWhereOrNull((query) {
return query.queryKey == queryKey && query is Query<T, Outside>;
})?.cast<Query<T, Outside>>();
}
Mutation<T, V>? getMutation<T extends Object, V>(String mutationKey) {
return _mutations.firstWhereOrNull((mutation) {
return mutation.mutationKey == mutationKey && mutation is Mutation<T, V>;
})?.cast<Mutation<T, V>>();
}
int get isFetching {
return _queries.fold<int>(
0,
(acc, query) {
if (query.isLoading || query.isRefetching) acc++;
return acc;
},
);
}
int get isMutating {
return _mutations.fold<int>(
0,
(acc, mutation) {
if (mutation.isLoading) acc++;
return acc;
},
);
}
void setQueryData<T extends Object, Outside>(
String queryKey, QueryUpdateFunction<T> updateCb) {
getQuery<T, Outside>(queryKey)?.setQueryData(updateCb);
}
void resetQueries(List<String> queryKeys) {
for (final query in _queries) {
if (!queryKeys.contains(query.queryKey)) continue;
query.reset();
}
}
void invalidateQueries(List<String> queryKeys) {
for (final query in _queries) {
if (!queryKeys.contains(query.queryKey)) continue;
// TODO: Implement Invaldiate Queries
}
}
Future<void> refetchQueries(List<String> queryKeys) async {
for (final query in _queries) {
if (!queryKeys.contains(query.queryKey)) continue;
await query.refetch();
}
}
static QueryBowl of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<QueryBowl>()!;
@override
bool updateShouldNotify(QueryBowl oldWidget) {
return oldWidget.staleTime != staleTime ||
oldWidget._queries != _queries ||
oldWidget._mutations != _mutations;
}
}
@@ -0,0 +1,102 @@
import 'package:fl_query/src/models/query_job.dart';
import 'package:fl_query/src/query.dart';
import 'package:fl_query/src/query_bowl.dart';
import 'package:fl_query/src/utils.dart';
import 'package:flutter/widgets.dart';
class QueryBuilder<T extends Object, Outside> extends StatefulWidget {
final Function(BuildContext, Query<T, Outside>) builder;
final QueryJob<T, Outside> job;
final Outside externalData;
/// Called when the query returns new data, on query
/// refetch or query gets expired
final QueryListener<T>? onData;
/// Called when the query returns error
final QueryListener<dynamic>? onError;
const QueryBuilder({
required this.job,
required this.externalData,
required this.builder,
this.onData,
this.onError,
Key? key,
}) : super(key: key);
@override
State<QueryBuilder<T, Outside>> createState() =>
_QueryBuilderState<T, Outside>();
}
class _QueryBuilderState<T extends Object, Outside>
extends State<QueryBuilder<T, Outside>> {
late QueryBowl queryBowl;
late final ValueKey<String> uKey;
late Query<T, Outside> query;
@override
void initState() {
super.initState();
uKey = ValueKey<String>(uuid.v4());
query = Query<T, Outside>.fromOptions(
widget.job,
externalData: widget.externalData,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
query = QueryBowl.of(context).addQuery<T, Outside>(
query,
key: uKey,
onData: widget.onData,
onError: widget.onError,
);
final hasExternalDataChanged = query.externalData != null &&
query.prevUsedExternalData != null &&
!isShallowEqual(query.externalData!, query.prevUsedExternalData!);
(query.fetched && query.refetchOnMount == true) || hasExternalDataChanged
? await query.refetch()
: await query.fetch();
});
}
@override
void didUpdateWidget(covariant oldWidget) {
if (oldWidget.externalData != null &&
widget.externalData != null &&
!isShallowEqual(oldWidget.externalData!, widget.externalData!)) {
QueryBowl.of(context).fetchQuery(
widget.job,
externalData: widget.externalData,
onData: widget.onData,
onError: widget.onError,
key: uKey,
);
} else {
if (oldWidget.onData != widget.onData && oldWidget.onData != null) {
query.onDataListeners.remove(oldWidget.onData);
if (widget.onData != null) query.onDataListeners.add(widget.onData!);
}
if (oldWidget.onError != widget.onError && oldWidget.onError != null) {
query.onErrorListeners.remove(oldWidget.onError);
if (widget.onError != null) query.onErrorListeners.add(widget.onError!);
}
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
query.unmount(uKey);
if (widget.onData != null) query.onDataListeners.remove(widget.onData);
if (widget.onError != null) query.onErrorListeners.remove(widget.onError);
super.dispose();
}
@override
Widget build(BuildContext context) {
queryBowl = QueryBowl.of(context);
final latestQuery = queryBowl.getQuery<T, Outside>(query.queryKey) ?? query;
return widget.builder(context, latestQuery);
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:fl_query/src/query.dart';
import 'package:uuid/uuid.dart';
Future<void> callQueryListeners<T>(Set<QueryListener<T>> listeners, T data) {
return Future.wait(listeners.map(
(listener) => Future.value(listener(data)),
));
// for (final listener in listeners) {
// await listener(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;
}
}