diff --git a/packages/fl_query/lib/src/collections/default_configs.dart b/packages/fl_query/lib/src/collections/default_configs.dart index 809dfc5..fa1f61a 100644 --- a/packages/fl_query/lib/src/collections/default_configs.dart +++ b/packages/fl_query/lib/src/collections/default_configs.dart @@ -2,6 +2,9 @@ import 'package:fl_query/src/collections/refresh_config.dart'; import 'package:fl_query/src/collections/retry_config.dart'; import 'package:flutter/material.dart'; +/// Default configurations for [RetryConfig], [RefreshConfig] and [Duration] +/// +/// This are opinionated defaults and can be overridden @immutable abstract class DefaultConstants { static const RetryConfig retryConfig = RetryConfig( diff --git a/packages/fl_query/lib/src/core/cache.dart b/packages/fl_query/lib/src/core/cache.dart index 0192349..c3dc1e8 100644 --- a/packages/fl_query/lib/src/core/cache.dart +++ b/packages/fl_query/lib/src/core/cache.dart @@ -17,6 +17,7 @@ enum QueryCacheEventType { removeMutation, } +/// A event triggered by the [QueryCache] cache modifications @immutable class QueryCacheEvent { final QueryCacheEventType type; @@ -25,6 +26,10 @@ class QueryCacheEvent { QueryCacheEvent(this.type, this.data); } +/// Cache for storing [Query], [InfiniteQuery] and [Mutation] objects +/// and triggering events when they are added or removed +/// +/// The cache can't be modified from outside directly class QueryCache { final Set _queries; final Set _infiniteQueries; @@ -40,6 +45,7 @@ class QueryCache { _infiniteQueries = Set(), _mutations = Set(), _eventController = StreamController.broadcast() { + // Invalidate inactive queries and mutations every [cacheDuration] Timer.periodic(cacheDuration, (timer) { _queries.removeWhere((query) { if (query.isInactive) { @@ -118,6 +124,7 @@ class QueryCache { ); } + /// Clears everything from cache void clear() { _queries.clear(); _infiniteQueries.clear(); diff --git a/packages/fl_query/lib/src/core/client.dart b/packages/fl_query/lib/src/core/client.dart index ab70ba5..9b068fa 100644 --- a/packages/fl_query/lib/src/core/client.dart +++ b/packages/fl_query/lib/src/core/client.dart @@ -13,6 +13,50 @@ import 'package:fl_query/src/core/query.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/adapters.dart'; +/// Base Client for managing [Query], [InfiniteQuery] and [Mutation] objects +/// and all related configuration +/// +/// [QueryClient] is the basic imperative API to handle and manage Queries and +/// Mutations and used internally by the Declarative wrapper widgets and hooks +/// +/// Usually, it can be helpful to use when only data modification is needed +/// without any UI changes e.g. after completing an action +/// +/// It can be accessed anywhere in the widget tree using [QueryClient.of] or +/// [QueryClient.maybeOf] +/// +/// ```dart +/// final queryClient = QueryClient.of(context); +/// +/// await queryClient.refreshQuery('todos'); +/// ``` +/// +/// If you don't have access to [BuildContext] e.g. in a [Provider] or [BLoC] +/// you can initialize your own [QueryClient] globally and pass it [QueryClientProvider] +/// and use it anywhere in the widget tree +/// +/// ```dart +/// final queryClient = QueryClient(); +/// +/// QueryClientProvider( +/// client: queryClient, +/// child: (....) +/// ) +/// +/// // Somewhere else in the project +/// import 'package:example/config/query_client.dart'; +/// +/// class TodoListNotifier extends ChangeNotifier { +/// void addTodo(Todo todo) async { +/// final res = post(api, todo); +/// +/// await queryClient.refreshQuery('todos'); +/// } +/// } +/// ``` +/// +/// * The above can be also implemented using just by [Mutation] + @immutable class QueryClient { final QueryCache cache; @@ -42,6 +86,10 @@ class QueryClient { refreshOnQueryFnChange: refreshOnQueryFnChange, ); + /// Imperatively creates a [Query] + /// + /// If a query with the same key already exists, it will be returned + /// and the properties will be updated (if changed) Query createQuery( String key, QueryFn queryFn, { @@ -68,6 +116,11 @@ class QueryClient { return query; } + /// Creates + stores a [Query] and runs the [queryFn] immediately + /// and returns the result + /// + /// - If fails, returns with `null` + /// - If [Query] already exists, it'll run the [Query.fetch] anyway Future fetchQuery( String key, QueryFn queryFn, { @@ -108,11 +161,17 @@ class QueryClient { } } + /// Finds the [Query] with the given [key] and returns it + /// + /// [exact] can be used to match the key exactly or by prefix Query? getQuery( - String key, - ) { + String key, { + bool exact = true, + }) { return cache.queries - .firstWhereOrNull((query) => query.key == key) + .firstWhereOrNull( + (query) => exact ? query.key == key : query.key.startsWith(key), + ) ?.cast(); } @@ -120,9 +179,21 @@ class QueryClient { return cache.queries.where((query) => keys.contains(query.key)).toList(); } - Future refreshQuery(String key, - {DataType? initial}) async { - final query = getQuery(key); + /// Finds all the [Query] that starts with the given [prefix] + List getQueriesWithPrefix(String prefix) { + return cache.queries + .where((query) => query.key.startsWith(prefix)) + .toList(); + } + + /// Finds the [Query] with the given [key] and refreshes using [Query.refresh] + /// + /// [exact] can be used to match the key exactly or by prefix + Future refreshQuery( + String key, { + bool exact = true, + }) async { + final query = getQuery(key, exact: exact); if (query == null) return null; return await query.refresh(); } @@ -132,6 +203,17 @@ class QueryClient { return await Future.wait(queries.map((query) => query.refresh())); } + /// Finds all the [Query] that starts with the given [prefix] + /// and refreshes + Future refreshQueriesWithPrefix(String prefix) async { + final queries = getQueriesWithPrefix(prefix); + return await Future.wait(queries.map((query) => query.refresh())); + } + + /// Creates + stores an [InfiniteQuery] + /// + /// If [InfinityQuery] already exists, it'll return the existing one + /// and update the configuration if changed InfiniteQuery createInfiniteQuery( String key, @@ -162,6 +244,11 @@ class QueryClient { return query; } + /// Creates + stores an [InfiniteQuery] and fetches the first page + /// immediately and returns the result + /// + /// - If fails, returns with `null` + /// - If [InfiniteQuery] already exists, it'll run the [InfiniteQuery.fetch] anyway Future fetchInfiniteQuery( String key, InfiniteQueryFn queryFn, { @@ -204,10 +291,17 @@ class QueryClient { } } + /// Finds the [InfiniteQuery] with the given [key] + /// + /// [exact] can be used to match the key exactly or by prefix InfiniteQuery? - getInfiniteQuery(String key) { + getInfiniteQuery( + String key, { + bool exact = true, + }) { return cache.infiniteQueries - .firstWhereOrNull((query) => query.key == key) + .firstWhereOrNull( + (query) => exact ? query.key == key : query.key.startsWith(key)) ?.cast(); } @@ -217,18 +311,46 @@ class QueryClient { .toList(); } + /// Finds all the [InfiniteQuery] that starts with the given [prefix] + List getInfiniteQueriesWithPrefix(String prefix) { + return cache.infiniteQueries + .where((query) => query.key.startsWith(prefix)) + .toList(); + } + + /// Finds the [InfiniteQuery] with the given [key] and refreshes + /// using [InfiniteQuery.refresh] + /// + /// It'll return the refreshed data and will return `null` if fails + /// + /// - [exact] can be used to match the key exactly or by prefix + /// - [page] can be used to only refresh a specific page or else it'll + /// refresh the lastPage + /// Future refreshInfiniteQuery( - String key, - [PageType? page]) async { - final query = getInfiniteQuery(key); + String key, { + PageType? page, + bool exact = true, + }) async { + final query = + getInfiniteQuery(key, exact: exact); if (query == null) return null; return await query.refresh(page); } + /// Finds the [InfiniteQuery] with the given [key] and refreshes all pages + /// using [InfiniteQuery.refreshAll] + /// + /// It'll return the refreshed data and will return `null` if fails + /// + /// - [exact] can be used to match the key exactly or by prefix Future?> refreshInfiniteQueryAllPages( - String key) async { - final query = getInfiniteQuery(key); + String key, { + bool exact = true, + }) async { + final query = + getInfiniteQuery(key, exact: exact); if (query == null) return []; return await query.refreshAll(); } @@ -238,14 +360,44 @@ class QueryClient { return await Future.wait(queries.map((query) => query.refresh())); } + /// Finds all the [InfiniteQuery] that starts with the given [prefix] + /// and refreshes using [InfiniteQuery.refresh] + /// + /// It'll return the refreshed data and will return `null` if fails + Future refreshInfiniteQueriesWithPrefix(String prefix) async { + final queries = getInfiniteQueriesWithPrefix(prefix); + return await Future.wait(queries.map((query) => query.refresh())); + } + Future> refreshInfiniteQueriesAllPages( - List keys) async { + List keys, + ) async { final queries = getInfiniteQueries(keys); return await Future.wait(queries.map( (query) async => MapEntry(query.key, await query.refreshAll()))) .then((qs) => Map.fromEntries(qs)); } + /// Finds all the [InfiniteQuery] that starts with the given [prefix] + /// and refreshes all pages using [InfiniteQuery.refreshAll] + /// + /// It returns a Map with the key as the matched query key and the value + /// as the refreshed data + /// + /// It'll return the refreshed data and will return `null` if fails + Future> refreshInfiniteQueriesAllPagesWithPrefix( + String prefix, + ) async { + final queries = getInfiniteQueriesWithPrefix(prefix); + return await Future.wait(queries.map( + (query) async => MapEntry(query.key, await query.refreshAll()))) + .then((qs) => Map.fromEntries(qs)); + } + + /// Creates a new [Mutation] + /// + /// If a [Mutation] with the same [key] already exists, it'll return the + /// existing [Mutation] and update the properties of the existing [Mutation] Mutation createMutation( String key, @@ -268,6 +420,17 @@ class QueryClient { return mutation; } + /// Finds the [Mutation] with the given [key] and runs [Mutation.mutate] + /// + /// It'll return the mutation result and will return `null` if fails + /// + /// Optionally takes an [mutationFn] to override the existing [mutationFn] + /// or create a completely new [Mutation] if doesn't exist + /// Same situation for [retryConfig] + /// + /// - [refreshQueries] can be used to refresh queries after mutation + /// - [refreshInfiniteQueries] can be used to refresh infinite queries + /// after mutation Future mutateMutation( String key, VariablesType variables, { @@ -313,19 +476,30 @@ class QueryClient { } } + /// Finds the [Mutation] + /// + /// - [exact] can be used to match the key exactly or by prefix Mutation? - getMutation(String key) { + getMutation( + String key, { + bool exact = true, + }) { return cache.mutations - .firstWhereOrNull((query) => query.key == key) + .firstWhereOrNull( + (query) => exact ? query.key == key : query.key.startsWith(key)) ?.cast(); } + /// Gets the [QueryClient] from the [BuildContext] if available + /// + /// This can throw an error if the [QueryClient] is not available static QueryClient of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType()! .client; } + /// Gets the [QueryClient] from the [BuildContext] if available static QueryClient? maybeOf(BuildContext context) { return context .dependOnInheritedWidgetOfExactType() @@ -338,6 +512,9 @@ class QueryClient { static String get infiniteQueryCachePrefix => '$_cachePrefix.cache.infinite_queries'; + /// Initializes the [QueryClient] + /// + /// This sets up all [Hive] boxes and cache directories static Future initialize({ required String cachePrefix, String? cacheDir,