import 'dart:async'; 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 { 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 createState() => _QueryBowlScopeState(); } class _QueryBowlScopeState extends State { late Set queries; late Set 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(Query query) { setState(() { queries = Set.from({...queries, query}); }); } void addMutation(Mutation mutation) { setState(() { mutations = Set.from({...mutations, mutation}); }); } int removeQueries(List 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(); mutations = Set(); }); } @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 _queries; final Set _mutations; final Duration staleTime; final Duration cacheTime; final Duration? refetchInterval; final bool refetchOnMount; final void Function(Query query) _addQuery; final void Function(Mutation mutation) _addMutation; final int Function(List) removeQueries; final void Function() clear; const QueryBowl({ required Widget child, required final void Function( Query query) addQuery, required final void Function(Mutation mutation) addMutation, required final Set queries, required final Set 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 fetchQuery( QueryJob options, { required Outside externalData, final QueryListener? onData, final QueryListener? onError, required ValueKey key, }) async { final prevQuery = _queries.firstWhereOrNull((q) => q.queryKey == options.queryKey); if (prevQuery is Query) { // 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.fromOptions( options, externalData: externalData, onData: onData, onError: onError, ); query.mount(key); _addQuery(query); return await query.fetch(); } @protected Query addQuery( Query query, { required ValueKey key, final QueryListener? onData, final QueryListener? onError, }) { final prevQuery = _queries.firstWhereOrNull((q) => q.queryKey == query.queryKey); if (prevQuery is Query) { // 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(query); return query; } @protected Mutation addMutation( Mutation mutation, { final MutationListener? onData, final MutationListener? onError, final MutationListener? onMutate, required ValueKey key, }) { final prevMutation = _mutations.firstWhereOrNull( (prevMutation) => prevMutation.mutationKey == mutation.mutationKey); if (prevMutation != null && prevMutation is Mutation) { 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? getQuery(String queryKey) { return _queries.firstWhereOrNull((query) { return query.queryKey == queryKey && query is Query; })?.cast>(); } Mutation? getMutation(String mutationKey) { return _mutations.firstWhereOrNull((mutation) { return mutation.mutationKey == mutationKey && mutation is Mutation; })?.cast>(); } int get isFetching { return _queries.fold( 0, (acc, query) { if (query.isLoading || query.isRefetching) acc++; return acc; }, ); } int get isMutating { return _mutations.fold( 0, (acc, mutation) { if (mutation.isLoading) acc++; return acc; }, ); } void setQueryData( String queryKey, QueryUpdateFunction updateCb) { getQuery(queryKey)?.setQueryData(updateCb); } void resetQueries(List queryKeys) { for (final query in _queries) { if (!queryKeys.contains(query.queryKey)) continue; query.reset(); } } void invalidateQueries(List queryKeys) { for (final query in _queries) { if (!queryKeys.contains(query.queryKey)) continue; // TODO: Implement Invaldiate Queries } } Future refetchQueries(List queryKeys) async { for (final query in _queries) { if (!queryKeys.contains(query.queryKey)) continue; await query.refetch(); } } static QueryBowl of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; @override bool updateShouldNotify(QueryBowl oldWidget) { return oldWidget.staleTime != staleTime || oldWidget._queries != _queries || oldWidget._mutations != _mutations; } }