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:fl_query/src/collections/retry_config.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default configurations for [RetryConfig], [RefreshConfig] and [Duration]
|
||||||
|
///
|
||||||
|
/// This are opinionated defaults and can be overridden
|
||||||
@immutable
|
@immutable
|
||||||
abstract class DefaultConstants {
|
abstract class DefaultConstants {
|
||||||
static const RetryConfig retryConfig = RetryConfig(
|
static const RetryConfig retryConfig = RetryConfig(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum QueryCacheEventType {
|
|||||||
removeMutation,
|
removeMutation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A event triggered by the [QueryCache] cache modifications
|
||||||
@immutable
|
@immutable
|
||||||
class QueryCacheEvent {
|
class QueryCacheEvent {
|
||||||
final QueryCacheEventType type;
|
final QueryCacheEventType type;
|
||||||
@@ -25,6 +26,10 @@ class QueryCacheEvent {
|
|||||||
QueryCacheEvent(this.type, this.data);
|
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 {
|
class QueryCache {
|
||||||
final Set<Query> _queries;
|
final Set<Query> _queries;
|
||||||
final Set<InfiniteQuery> _infiniteQueries;
|
final Set<InfiniteQuery> _infiniteQueries;
|
||||||
@@ -40,6 +45,7 @@ class QueryCache {
|
|||||||
_infiniteQueries = Set<InfiniteQuery>(),
|
_infiniteQueries = Set<InfiniteQuery>(),
|
||||||
_mutations = Set<Mutation>(),
|
_mutations = Set<Mutation>(),
|
||||||
_eventController = StreamController<QueryCacheEvent>.broadcast() {
|
_eventController = StreamController<QueryCacheEvent>.broadcast() {
|
||||||
|
// Invalidate inactive queries and mutations every [cacheDuration]
|
||||||
Timer.periodic(cacheDuration, (timer) {
|
Timer.periodic(cacheDuration, (timer) {
|
||||||
_queries.removeWhere((query) {
|
_queries.removeWhere((query) {
|
||||||
if (query.isInactive) {
|
if (query.isInactive) {
|
||||||
@@ -118,6 +124,7 @@ class QueryCache {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears everything from cache
|
||||||
void clear() {
|
void clear() {
|
||||||
_queries.clear();
|
_queries.clear();
|
||||||
_infiniteQueries.clear();
|
_infiniteQueries.clear();
|
||||||
|
|||||||
@@ -13,6 +13,50 @@ import 'package:fl_query/src/core/query.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/adapters.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
|
@immutable
|
||||||
class QueryClient {
|
class QueryClient {
|
||||||
final QueryCache cache;
|
final QueryCache cache;
|
||||||
@@ -42,6 +86,10 @@ class QueryClient {
|
|||||||
refreshOnQueryFnChange: refreshOnQueryFnChange,
|
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>(
|
Query<DataType, ErrorType> createQuery<DataType, ErrorType>(
|
||||||
String key,
|
String key,
|
||||||
QueryFn<DataType> queryFn, {
|
QueryFn<DataType> queryFn, {
|
||||||
@@ -68,6 +116,11 @@ class QueryClient {
|
|||||||
return query;
|
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>(
|
Future<DataType?> fetchQuery<DataType, ErrorType>(
|
||||||
String key,
|
String key,
|
||||||
QueryFn<DataType> queryFn, {
|
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>(
|
Query<DataType, ErrorType>? getQuery<DataType, ErrorType>(
|
||||||
String key,
|
String key, {
|
||||||
) {
|
bool exact = true,
|
||||||
|
}) {
|
||||||
return cache.queries
|
return cache.queries
|
||||||
.firstWhereOrNull((query) => query.key == key)
|
.firstWhereOrNull(
|
||||||
|
(query) => exact ? query.key == key : query.key.startsWith(key),
|
||||||
|
)
|
||||||
?.cast<DataType, ErrorType>();
|
?.cast<DataType, ErrorType>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +179,21 @@ class QueryClient {
|
|||||||
return cache.queries.where((query) => keys.contains(query.key)).toList();
|
return cache.queries.where((query) => keys.contains(query.key)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DataType?> refreshQuery<DataType, ErrorType>(String key,
|
/// Finds all the [Query] that starts with the given [prefix]
|
||||||
{DataType? initial}) async {
|
List<Query> getQueriesWithPrefix(String prefix) {
|
||||||
final query = getQuery<DataType, ErrorType>(key);
|
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;
|
if (query == null) return null;
|
||||||
return await query.refresh();
|
return await query.refresh();
|
||||||
}
|
}
|
||||||
@@ -132,6 +203,17 @@ class QueryClient {
|
|||||||
return await Future.wait(queries.map((query) => query.refresh()));
|
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>
|
InfiniteQuery<DataType, ErrorType, PageType>
|
||||||
createInfiniteQuery<DataType, ErrorType, PageType>(
|
createInfiniteQuery<DataType, ErrorType, PageType>(
|
||||||
String key,
|
String key,
|
||||||
@@ -162,6 +244,11 @@ class QueryClient {
|
|||||||
return query;
|
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>(
|
Future<DataType?> fetchInfiniteQuery<DataType, ErrorType, PageType>(
|
||||||
String key,
|
String key,
|
||||||
InfiniteQueryFn<DataType, PageType> queryFn, {
|
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>?
|
InfiniteQuery<DataType, ErrorType, PageType>?
|
||||||
getInfiniteQuery<DataType, ErrorType, PageType>(String key) {
|
getInfiniteQuery<DataType, ErrorType, PageType>(
|
||||||
|
String key, {
|
||||||
|
bool exact = true,
|
||||||
|
}) {
|
||||||
return cache.infiniteQueries
|
return cache.infiniteQueries
|
||||||
.firstWhereOrNull((query) => query.key == key)
|
.firstWhereOrNull(
|
||||||
|
(query) => exact ? query.key == key : query.key.startsWith(key))
|
||||||
?.cast<DataType, ErrorType, PageType>();
|
?.cast<DataType, ErrorType, PageType>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,18 +311,46 @@ class QueryClient {
|
|||||||
.toList();
|
.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>(
|
Future<DataType?> refreshInfiniteQuery<DataType, ErrorType, PageType>(
|
||||||
String key,
|
String key, {
|
||||||
[PageType? page]) async {
|
PageType? page,
|
||||||
final query = getInfiniteQuery<DataType, ErrorType, PageType>(key);
|
bool exact = true,
|
||||||
|
}) async {
|
||||||
|
final query =
|
||||||
|
getInfiniteQuery<DataType, ErrorType, PageType>(key, exact: exact);
|
||||||
if (query == null) return null;
|
if (query == null) return null;
|
||||||
return await query.refresh(page);
|
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>?>
|
Future<List<DataType>?>
|
||||||
refreshInfiniteQueryAllPages<DataType, ErrorType, PageType>(
|
refreshInfiniteQueryAllPages<DataType, ErrorType, PageType>(
|
||||||
String key) async {
|
String key, {
|
||||||
final query = getInfiniteQuery<DataType, ErrorType, PageType>(key);
|
bool exact = true,
|
||||||
|
}) async {
|
||||||
|
final query =
|
||||||
|
getInfiniteQuery<DataType, ErrorType, PageType>(key, exact: exact);
|
||||||
if (query == null) return [];
|
if (query == null) return [];
|
||||||
return await query.refreshAll();
|
return await query.refreshAll();
|
||||||
}
|
}
|
||||||
@@ -238,14 +360,44 @@ class QueryClient {
|
|||||||
return await Future.wait(queries.map((query) => query.refresh()));
|
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(
|
Future<Map<String, List?>> refreshInfiniteQueriesAllPages(
|
||||||
List<String> keys) async {
|
List<String> keys,
|
||||||
|
) async {
|
||||||
final queries = getInfiniteQueries(keys);
|
final queries = getInfiniteQueries(keys);
|
||||||
return await Future.wait(queries.map(
|
return await Future.wait(queries.map(
|
||||||
(query) async => MapEntry(query.key, await query.refreshAll())))
|
(query) async => MapEntry(query.key, await query.refreshAll())))
|
||||||
.then((qs) => Map.fromEntries(qs));
|
.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>
|
Mutation<DataType, ErrorType, VariablesType>
|
||||||
createMutation<DataType, ErrorType, VariablesType>(
|
createMutation<DataType, ErrorType, VariablesType>(
|
||||||
String key,
|
String key,
|
||||||
@@ -268,6 +420,17 @@ class QueryClient {
|
|||||||
return mutation;
|
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>(
|
Future<DataType?> mutateMutation<DataType, ErrorType, VariablesType>(
|
||||||
String key,
|
String key,
|
||||||
VariablesType variables, {
|
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>?
|
Mutation<DataType, ErrorType, VariablesType>?
|
||||||
getMutation<DataType, ErrorType, VariablesType>(String key) {
|
getMutation<DataType, ErrorType, VariablesType>(
|
||||||
|
String key, {
|
||||||
|
bool exact = true,
|
||||||
|
}) {
|
||||||
return cache.mutations
|
return cache.mutations
|
||||||
.firstWhereOrNull((query) => query.key == key)
|
.firstWhereOrNull(
|
||||||
|
(query) => exact ? query.key == key : query.key.startsWith(key))
|
||||||
?.cast<DataType, ErrorType, VariablesType>();
|
?.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) {
|
static QueryClient of(BuildContext context) {
|
||||||
return context
|
return context
|
||||||
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()!
|
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()!
|
||||||
.client;
|
.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the [QueryClient] from the [BuildContext] if available
|
||||||
static QueryClient? maybeOf(BuildContext context) {
|
static QueryClient? maybeOf(BuildContext context) {
|
||||||
return context
|
return context
|
||||||
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()
|
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()
|
||||||
@@ -338,6 +512,9 @@ class QueryClient {
|
|||||||
static String get infiniteQueryCachePrefix =>
|
static String get infiniteQueryCachePrefix =>
|
||||||
'$_cachePrefix.cache.infinite_queries';
|
'$_cachePrefix.cache.infinite_queries';
|
||||||
|
|
||||||
|
/// Initializes the [QueryClient]
|
||||||
|
///
|
||||||
|
/// This sets up all [Hive] boxes and cache directories
|
||||||
static Future<void> initialize({
|
static Future<void> initialize({
|
||||||
required String cachePrefix,
|
required String cachePrefix,
|
||||||
String? cacheDir,
|
String? cacheDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user