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: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();
+193 -16
View File
@@ -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,