import 'dart:async'; import 'package:fl_query/models/mutation_job.dart'; 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}); }); } @override Widget build(BuildContext context) { _disposeUpdateListeners(); _listenToUpdates(); return QueryBowl( addQuery: addQuery, addMutation: addMutation, 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; 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, Key? key, }) : _addQuery = addQuery, _queries = queries, _mutations = mutations, _addMutation = addMutation, super(child: child, key: key); 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(); } 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; } 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 resetQuery(String queryKey) { _queries .firstWhereOrNull((element) => element.queryKey == queryKey) ?.reset(); } static QueryBowl of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; @override bool updateShouldNotify(QueryBowl oldWidget) { return oldWidget.staleTime != staleTime || oldWidget._queries != _queries || oldWidget._mutations != _mutations; } }