feat: add ability to get, refresh query and infinite queries using prefix
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user