import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fl_query/fl_query_hooks.dart'; import 'package:fl_query/src/models/query_job.dart'; import 'package:fl_query/src/mutation.dart'; import 'package:fl_query/src/mutation_builder.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'; /// The widget that holds & provides every [Query] & [Mutation] to your /// entire Flutter application in anywhere /// This must be used on the above any other widget /// /// ```dart /// Widget build(BuildContext context) { /// return QueryBowlScope( /// child: MaterialApp(/*...other stuff...*/), /// ); /// } /// ``` /// class QueryBowlScope extends StatefulWidget { final Widget child; /// global stale time /// /// Makes [Query.data] stale after crossing the duration of provided /// [staleTime] final Duration staleTime; /// global cache time /// /// Removes inactive queries after provided duration of [cacheTime] 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; /// the delay to call each query when user device reconnects to the /// internet. Using a delay after each refetch so refetching all the /// queries at once won't create high CPU spikes & also wouldn't violate /// rate-limit /// /// Though its recommended most of the time to use but it can be turned /// off by passing [Duration.zero] final Duration refetchOnReconnectDelay; /// 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.refetchOnReconnectDelay = const Duration(milliseconds: 100), 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; StreamSubscription? _connectionStatusSubscription; @override void initState() { super.initState(); queries = {}; mutations = {}; _connectionStatusSubscription = Connectivity() .onConnectivityChanged .listen((ConnectivityResult result) async { if (isConnectedToInternet(result)) { for (final query in queries) { if (query.refetchOnReconnect == false || !query.enabled) continue; await query.refetch(); await Future.delayed(widget.refetchOnReconnectDelay); } } }); } @override void dispose() { _disposeUpdateListeners(); _connectionStatusSubscription?.cancel(); 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) { WidgetsBinding.instance.addPostFrameCallback((_) { // checking & not including inactive queries // basically garbage collecting queries setState(() { queries = Set.from( query.isInactive ? queries.where((el) => el.queryKey != query.queryKey) : queries, ); }); }); } void updateMutations(Mutation mutation) { WidgetsBinding.instance.addPostFrameCallback((_) { 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) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { queries = Set.from({...queries, query}); }); }); } void addMutation(Mutation mutation) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { mutations = Set.from({...mutations, mutation}); }); }); } int removeQueries(List queryKeys) { int count = 0; WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { mutations = Set.from( queries.whereNot((query) { final isAboutToRip = queryKeys.contains(query.queryKey); if (isAboutToRip) count++; return isAboutToRip; }), ); }); }); return count; } void clear() { WidgetsBinding.instance.addPostFrameCallback((_) { 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, refetchOnReconnect: widget.refetchOnReconnect, child: widget.child, ); } } /// QueryBowl provides an imperative way to handle all the query & /// mutation related methods & properties. /// /// Its responsible or can be used for (not recommended) creating, /// updating & deleting queries & mutations class QueryBowl extends InheritedWidget { final Set _queries; final Set _mutations; final Duration staleTime; final Duration cacheTime; final Duration? refetchInterval; final bool refetchOnMount; final bool refetchOnReconnect; 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, required this.refetchOnReconnect, this.refetchInterval, Key? key, }) : _addQuery = addQuery, _queries = queries, _mutations = mutations, _addMutation = addMutation, super(child: child, key: key); /// !⚠️**Warning** only for internal library usage @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 await prevQuery.refetch(); } // 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; options.refetchOnReconnect ??= refetchOnReconnect; final query = Query.fromOptions( options, externalData: externalData, queryBowl: this, onData: onData, onError: onError, ); query.mount(key); _addQuery(query); return await query.fetch(); } /// !⚠️**Warning** only for internal library usage @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, refetchOnReconnect: refetchOnReconnect, ); query.mount(key); _addQuery(query); return query; } /// !⚠️**Warning** only for internal library usage @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 { if (onData != null) mutation.onDataListeners.add(onData); if (onError != null) mutation.onErrorListeners.add(onError); if (onMutate != null) mutation.onMutateListeners.add(onMutate); mutation.updateDefaultOptions(cacheTime: cacheTime); mutation.mount(key); _addMutation(mutation); return mutation; } } /// Get a query by providing queryKey only /// /// Useful for optimistic update or single query refetch Query? getQuery(String queryKey) { return _queries.firstWhereOrNull((query) { return query.queryKey == queryKey && query is Query; })?.cast>(); } /// Get a mutation by providing mutationKey only /// /// Useful for mutation resets Mutation? getMutation(String mutationKey) { return _mutations.firstWhereOrNull((mutation) { return mutation.mutationKey == mutationKey && mutation is Mutation; })?.cast>(); } /// Returns the number of query is currently fetching or refetching int get isFetching { return _queries.fold( 0, (acc, query) { if (query.isLoading || query.isRefetching) acc++; return acc; }, ); } /// Provides the number of mutations that are running at the moment int get isMutating { return _mutations.fold( 0, (acc, mutation) { if (mutation.isLoading) acc++; return acc; }, ); } /// Sets [Query]'s data manually /// /// Mostly used in combination with [onMutate] callback of /// [MutationBuilder] & [useMutation] for optimistic updates void setQueryData( String queryKey, QueryUpdateFunction updateCb) { getQuery(queryKey)?.setQueryData(updateCb); } /// resets all the queries matching the passed List of queryKeys void resetQueries(List queryKeys) { for (final query in _queries) { if (!queryKeys.contains(query.queryKey)) continue; query.reset(); } } /// makes all the queries matching the passed List of queryKeys stale void invalidateQueries(List queryKeys) { for (final query in _queries) { if (!queryKeys.contains(query.queryKey)) continue; // TODO: Implement Invaldiate Queries } } /// refetches all the queries matching the passed List of queryKeys Future refetchQueries(List queryKeys) async { for (final query in _queries) { if (queryKeys.contains(query.queryKey)) { await query.refetch(); } } } static QueryBowl of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()!; /// !⚠️**Warning** only for internal library usage @override @protected bool updateShouldNotify(QueryBowl oldWidget) { return oldWidget.staleTime != staleTime || oldWidget._queries != _queries || oldWidget._mutations != _mutations; } }