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:flutter/widgets.dart'; class QueryBowlScope extends StatefulWidget { final Widget child; final Duration staleTime; final Duration cacheTime; /// used for periodically checking if any query got stale. /// If none is supplied then half of the value of staleTime is used final Duration refreshInterval; const QueryBowlScope({ required this.child, this.staleTime = const Duration(milliseconds: 500), this.cacheTime = const Duration(minutes: 5), this.refreshInterval = const Duration(minutes: 5), Key? key, }) : super(key: key); @override State createState() => _QueryBowlScopeState(); } class _QueryBowlScopeState extends State { late Set queries; late Set mutations; late Timer refreshIntervalTimer; @override void initState() { super.initState(); queries = {}; mutations = {}; refreshIntervalTimer = Timer.periodic( widget.refreshInterval, _checkAndUpdateStaleQueriesOnBg, ); } @override void dispose() { refreshIntervalTimer.cancel(); _disposeUpdateListeners(); super.dispose(); } Future _checkAndUpdateStaleQueriesOnBg([dynamic _]) async { // checking for staled queries inside the widget as InheritedWidget // classes has to be constant & doesn't this kind of dynamic behavior for (final query in queries) { if (query.isStale) await query.refetch(); } } void _listenToUpdates() { for (final query in queries) { query.addListener(() => updateQueries(query)); } 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, 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 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.removeQueries, required this.clear, 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 != 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; } 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 != 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.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.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; } }