feat: add ability to get, refresh query and infinite queries using prefix

This commit is contained in:
Kingkor Roy Tirtho
2023-06-09 22:51:50 +06:00
parent 1b1f1cdc93
commit bc52de5303
3 changed files with 203 additions and 16 deletions
@@ -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(
@@ -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<Query> _queries;
final Set<InfiniteQuery> _infiniteQueries;
@@ -40,6 +45,7 @@ class QueryCache {
_infiniteQueries = Set<InfiniteQuery>(),
_mutations = Set<Mutation>(),
_eventController = StreamController<QueryCacheEvent>.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();
+193 -16
View File
@@ -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<DataType, ErrorType> createQuery<DataType, ErrorType>(
String key,
QueryFn<DataType> 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<DataType?> fetchQuery<DataType, ErrorType>(
String key,
QueryFn<DataType> 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<DataType, ErrorType>? getQuery<DataType, ErrorType>(
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<DataType, ErrorType>();
}
@@ -120,9 +179,21 @@ class QueryClient {
return cache.queries.where((query) => keys.contains(query.key)).toList();
}
Future<DataType?> refreshQuery<DataType, ErrorType>(String key,
{DataType? initial}) async {
final query = getQuery<DataType, ErrorType>(key);
/// Finds all the [Query] that starts with the given [prefix]
List<Query> 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<DataType?> refreshQuery<DataType, ErrorType>(
String key, {
bool exact = true,
}) async {
final query = getQuery<DataType, ErrorType>(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<List> 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<DataType, ErrorType, PageType>
createInfiniteQuery<DataType, ErrorType, PageType>(
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<DataType?> fetchInfiniteQuery<DataType, ErrorType, PageType>(
String key,
InfiniteQueryFn<DataType, PageType> 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<DataType, ErrorType, PageType>?
getInfiniteQuery<DataType, ErrorType, PageType>(String key) {
getInfiniteQuery<DataType, ErrorType, PageType>(
String key, {
bool exact = true,
}) {
return cache.infiniteQueries
.firstWhereOrNull((query) => query.key == key)
.firstWhereOrNull(
(query) => exact ? query.key == key : query.key.startsWith(key))
?.cast<DataType, ErrorType, PageType>();
}
@@ -217,18 +311,46 @@ class QueryClient {
.toList();
}
/// Finds all the [InfiniteQuery] that starts with the given [prefix]
List<InfiniteQuery> 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<DataType?> refreshInfiniteQuery<DataType, ErrorType, PageType>(
String key,
[PageType? page]) async {
final query = getInfiniteQuery<DataType, ErrorType, PageType>(key);
String key, {
PageType? page,
bool exact = true,
}) async {
final query =
getInfiniteQuery<DataType, ErrorType, PageType>(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<List<DataType>?>
refreshInfiniteQueryAllPages<DataType, ErrorType, PageType>(
String key) async {
final query = getInfiniteQuery<DataType, ErrorType, PageType>(key);
String key, {
bool exact = true,
}) async {
final query =
getInfiniteQuery<DataType, ErrorType, PageType>(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<List> refreshInfiniteQueriesWithPrefix(String prefix) async {
final queries = getInfiniteQueriesWithPrefix(prefix);
return await Future.wait(queries.map((query) => query.refresh()));
}
Future<Map<String, List?>> refreshInfiniteQueriesAllPages(
List<String> keys) async {
List<String> 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<Map<String, List?>> 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<DataType, ErrorType, VariablesType>
createMutation<DataType, ErrorType, VariablesType>(
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<DataType?> mutateMutation<DataType, ErrorType, VariablesType>(
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<DataType, ErrorType, VariablesType>?
getMutation<DataType, ErrorType, VariablesType>(String key) {
getMutation<DataType, ErrorType, VariablesType>(
String key, {
bool exact = true,
}) {
return cache.mutations
.firstWhereOrNull((query) => query.key == key)
.firstWhereOrNull(
(query) => exact ? query.key == key : query.key.startsWith(key))
?.cast<DataType, ErrorType, VariablesType>();
}
/// 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<QueryClientProvider>()!
.client;
}
/// Gets the [QueryClient] from the [BuildContext] if available
static QueryClient? maybeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()
@@ -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<void> initialize({
required String cachePrefix,
String? cacheDir,