files moved to src folder
added flutter_hooks support with example
This commit is contained in:
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user