feat: cache, client, query builder add and refreshOnQueryFnChange option
fix: retryer wrong logic
This commit is contained in:
@@ -12,5 +12,8 @@ abstract class DefaultConstants {
|
||||
staleDuration: Duration(seconds: 10),
|
||||
refreshInterval: Duration(seconds: 5),
|
||||
refreshOnMount: true,
|
||||
refreshOnQueryFnChange: false,
|
||||
);
|
||||
|
||||
static const Duration cacheDuration = Duration(minutes: 5);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ class RefreshConfig {
|
||||
final Duration staleDuration;
|
||||
final Duration refreshInterval;
|
||||
final bool refreshOnMount;
|
||||
final bool refreshOnQueryFnChange;
|
||||
|
||||
const RefreshConfig({
|
||||
required this.staleDuration,
|
||||
required this.refreshInterval,
|
||||
required this.refreshOnMount,
|
||||
required this.refreshOnQueryFnChange,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1,43 @@
|
||||
class QueryCache {}
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query/src/collections/default_configs.dart';
|
||||
import 'package:fl_query/src/core/infinite_query.dart';
|
||||
import 'package:fl_query/src/core/query.dart';
|
||||
|
||||
class QueryCache {
|
||||
final Set<Query> _queries;
|
||||
final Set<InfiniteQuery> _infiniteQueries;
|
||||
|
||||
final Duration cacheDuration;
|
||||
|
||||
QueryCache({
|
||||
this.cacheDuration = DefaultConstants.cacheDuration,
|
||||
}) : _queries = Set<Query>(),
|
||||
_infiniteQueries = Set<InfiniteQuery>() {
|
||||
Timer.periodic(cacheDuration, (timer) {
|
||||
_queries.removeWhere((query) => query.isInactive);
|
||||
_infiniteQueries.removeWhere((query) => query.isInactive);
|
||||
});
|
||||
}
|
||||
|
||||
UnmodifiableSetView<Query> get queries => UnmodifiableSetView(_queries);
|
||||
UnmodifiableSetView<InfiniteQuery> get infiniteQueries =>
|
||||
UnmodifiableSetView(_infiniteQueries);
|
||||
|
||||
void addQuery(Query query) {
|
||||
_queries.add(query);
|
||||
}
|
||||
|
||||
void addInfiniteQuery(InfiniteQuery query) {
|
||||
_infiniteQueries.add(query);
|
||||
}
|
||||
|
||||
void removeQuery(Query query) {
|
||||
_queries.remove(query);
|
||||
}
|
||||
|
||||
void removeInfiniteQuery(InfiniteQuery query) {
|
||||
_infiniteQueries.remove(query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,101 @@
|
||||
class QueryClient {}
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query/src/collections/default_configs.dart';
|
||||
import 'package:fl_query/src/collections/json_config.dart';
|
||||
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
import 'package:fl_query/src/core/cache.dart';
|
||||
import 'package:fl_query/src/core/provider.dart';
|
||||
import 'package:fl_query/src/core/query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
|
||||
class QueryClient {
|
||||
final QueryCache cache;
|
||||
|
||||
QueryClient({QueryCache? cache}) : this.cache = cache ?? QueryCache();
|
||||
|
||||
Query<DataType, ErrorType, KeyType> createQuery<DataType, ErrorType, KeyType>(
|
||||
ValueKey<KeyType> key,
|
||||
QueryFn<DataType> queryFn, {
|
||||
DataType? initial,
|
||||
RetryConfig retryConfig = DefaultConstants.retryConfig,
|
||||
RefreshConfig refreshConfig = DefaultConstants.refreshConfig,
|
||||
JsonConfig<DataType>? jsonConfig,
|
||||
}) {
|
||||
final query = cache.queries
|
||||
.firstWhere(
|
||||
(query) => query.key == key,
|
||||
orElse: () => Query<DataType, ErrorType, KeyType>(
|
||||
key,
|
||||
queryFn,
|
||||
initial: initial,
|
||||
retryConfig: retryConfig,
|
||||
refreshConfig: refreshConfig,
|
||||
jsonConfig: jsonConfig,
|
||||
),
|
||||
)
|
||||
.cast<DataType, ErrorType, KeyType>();
|
||||
cache.addQuery(query);
|
||||
return query;
|
||||
}
|
||||
|
||||
Future<DataType?> fetchQuery<DataType, ErrorType, KeyType>(
|
||||
ValueKey<KeyType> key,
|
||||
QueryFn<DataType> queryFn, {
|
||||
DataType? initial,
|
||||
RetryConfig retryConfig = DefaultConstants.retryConfig,
|
||||
RefreshConfig refreshConfig = DefaultConstants.refreshConfig,
|
||||
JsonConfig<DataType>? jsonConfig,
|
||||
}) async {
|
||||
final query = createQuery<DataType, ErrorType, KeyType>(
|
||||
key,
|
||||
queryFn,
|
||||
initial: initial,
|
||||
retryConfig: retryConfig,
|
||||
refreshConfig: refreshConfig,
|
||||
jsonConfig: jsonConfig,
|
||||
);
|
||||
return await query.fetch();
|
||||
}
|
||||
|
||||
Query<DataType, ErrorType, KeyType>? getQuery<DataType, ErrorType, KeyType>(
|
||||
ValueKey<KeyType> key) {
|
||||
return cache.queries
|
||||
.firstWhereOrNull((query) => query.key == key)
|
||||
?.cast<DataType, ErrorType, KeyType>();
|
||||
}
|
||||
|
||||
List<Query> getQueries(List<ValueKey> keys) {
|
||||
return cache.queries.where((query) => keys.contains(query.key)).toList();
|
||||
}
|
||||
|
||||
Future<DataType?> refreshQuery<DataType, ErrorType, KeyType>(
|
||||
ValueKey<KeyType> key,
|
||||
{DataType? initial}) async {
|
||||
final query = getQuery<DataType, ErrorType, KeyType>(key);
|
||||
if (query == null) return null;
|
||||
return await query.refresh();
|
||||
}
|
||||
|
||||
Future<List> refreshQueries(List<ValueKey> keys) async {
|
||||
final queries = getQueries(keys);
|
||||
return await Future.wait(queries.map((query) => query.refresh()));
|
||||
}
|
||||
|
||||
static QueryClient of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()!
|
||||
.client;
|
||||
}
|
||||
|
||||
static QueryClient? maybeOf(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<QueryClientProvider>()
|
||||
?.client;
|
||||
}
|
||||
|
||||
static Future<void> initialize({String? cacheDir}) async {
|
||||
await Hive.initFlutter(cacheDir);
|
||||
await Hive.openLazyBox('cache');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,22 @@ import 'package:fl_query/src/collections/json_config.dart';
|
||||
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
import 'package:fl_query/src/core/retryer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Listener;
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:state_notifier/state_notifier.dart';
|
||||
|
||||
typedef InfiniteQueryFn<T, P> = FutureOr<T?> Function(P page);
|
||||
typedef InfiniteQueryNextPage<T, P> = P? Function(
|
||||
P lastPage,
|
||||
List<T> pages,
|
||||
typedef InfiniteQueryFn<DataType, PageType> = FutureOr<DataType?> Function(
|
||||
PageType page);
|
||||
typedef InfiniteQueryNextPage<DataType, PageType> = PageType? Function(
|
||||
PageType lastPage,
|
||||
List<DataType> pages,
|
||||
);
|
||||
|
||||
class InfiniteQueryPage<T, E, P> {
|
||||
final P page;
|
||||
final T? data;
|
||||
final E? error;
|
||||
class InfiniteQueryPage<DataType, ErrorType, PageType> {
|
||||
final PageType page;
|
||||
final DataType? data;
|
||||
final ErrorType? error;
|
||||
|
||||
final DateTime updatedAt;
|
||||
final Duration staleDuration;
|
||||
@@ -35,11 +36,11 @@ class InfiniteQueryPage<T, E, P> {
|
||||
|
||||
bool get isStale => DateTime.now().difference(updatedAt) > staleDuration;
|
||||
|
||||
InfiniteQueryPage<T, E, P> copyWith({
|
||||
T? data,
|
||||
E? error,
|
||||
InfiniteQueryPage<DataType, ErrorType, PageType> copyWith({
|
||||
DataType? data,
|
||||
ErrorType? error,
|
||||
}) {
|
||||
return InfiniteQueryPage<T, E, P>(
|
||||
return InfiniteQueryPage<DataType, ErrorType, PageType>(
|
||||
page: page,
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: staleDuration,
|
||||
@@ -52,17 +53,18 @@ class InfiniteQueryPage<T, E, P> {
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is InfiniteQueryPage<T, E, P> && other.page == page;
|
||||
return other is InfiniteQueryPage<DataType, ErrorType, PageType> &&
|
||||
other.page == page;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => page.hashCode;
|
||||
}
|
||||
|
||||
class InfiniteQueryState<T, E, P> {
|
||||
final Set<InfiniteQueryPage<T, E, P>> pages;
|
||||
final InfiniteQueryFn<T, P> queryFn;
|
||||
final InfiniteQueryNextPage<T, P> nextPage;
|
||||
class InfiniteQueryState<DataType, ErrorType, PageType> {
|
||||
final Set<InfiniteQueryPage<DataType, ErrorType, PageType>> pages;
|
||||
final InfiniteQueryFn<DataType, PageType> queryFn;
|
||||
final InfiniteQueryNextPage<DataType, PageType> nextPage;
|
||||
|
||||
const InfiniteQueryState({
|
||||
required this.pages,
|
||||
@@ -70,16 +72,17 @@ class InfiniteQueryState<T, E, P> {
|
||||
required this.nextPage,
|
||||
});
|
||||
|
||||
P get lastPage => pages.last.page;
|
||||
P? get getNextPage => nextPage(lastPage, pages.map((e) => e.data!).toList());
|
||||
PageType get lastPage => pages.last.page;
|
||||
PageType? get getNextPage =>
|
||||
nextPage(lastPage, pages.map((e) => e.data!).toList());
|
||||
bool get hasNextPage => getNextPage != null;
|
||||
|
||||
InfiniteQueryState<T, E, P> copyWith({
|
||||
Set<InfiniteQueryPage<T, E, P>>? pages,
|
||||
InfiniteQueryFn<T, P>? queryFn,
|
||||
InfiniteQueryNextPage<T, P>? nextPage,
|
||||
InfiniteQueryState<DataType, ErrorType, PageType> copyWith({
|
||||
Set<InfiniteQueryPage<DataType, ErrorType, PageType>>? pages,
|
||||
InfiniteQueryFn<DataType, PageType>? queryFn,
|
||||
InfiniteQueryNextPage<DataType, PageType>? nextPage,
|
||||
}) {
|
||||
return InfiniteQueryState<T, E, P>(
|
||||
return InfiniteQueryState<DataType, ErrorType, PageType>(
|
||||
pages: pages ?? this.pages,
|
||||
queryFn: queryFn ?? this.queryFn,
|
||||
nextPage: nextPage ?? this.nextPage,
|
||||
@@ -87,24 +90,25 @@ class InfiniteQueryState<T, E, P> {
|
||||
}
|
||||
}
|
||||
|
||||
class InfiniteQuery<T, E, K, P>
|
||||
extends StateNotifier<InfiniteQueryState<T, E, P>> with Retryer<T, E> {
|
||||
final ValueKey<K> key;
|
||||
class InfiniteQuery<DataType, ErrorType, KeyType, PageType>
|
||||
extends StateNotifier<InfiniteQueryState<DataType, ErrorType, PageType>>
|
||||
with Retryer<DataType, ErrorType> {
|
||||
final ValueKey<KeyType> key;
|
||||
final RetryConfig retryConfig;
|
||||
final RefreshConfig refreshConfig;
|
||||
final JsonConfig<T>? jsonConfig;
|
||||
final JsonConfig<DataType>? jsonConfig;
|
||||
|
||||
InfiniteQuery(
|
||||
this.key,
|
||||
InfiniteQueryFn<T, P> queryFn, {
|
||||
required InfiniteQueryNextPage<T, P> nextPage,
|
||||
required P initialParam,
|
||||
InfiniteQueryFn<DataType, PageType> queryFn, {
|
||||
required InfiniteQueryNextPage<DataType, PageType> nextPage,
|
||||
required PageType initialParam,
|
||||
this.retryConfig = DefaultConstants.retryConfig,
|
||||
this.refreshConfig = DefaultConstants.refreshConfig,
|
||||
this.jsonConfig,
|
||||
}) : super(InfiniteQueryState<T, E, P>(
|
||||
}) : super(InfiniteQueryState<DataType, ErrorType, PageType>(
|
||||
pages: {
|
||||
InfiniteQueryPage<T, E, P>(
|
||||
InfiniteQueryPage<DataType, ErrorType, PageType>(
|
||||
page: initialParam,
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: refreshConfig.staleDuration,
|
||||
@@ -115,12 +119,12 @@ class InfiniteQuery<T, E, K, P>
|
||||
)) {
|
||||
if (jsonConfig != null) {
|
||||
_mutex.protect(() async {
|
||||
final json = await _box.get(key.value);
|
||||
final json = await _box.get(key.toString());
|
||||
if (json != null) {
|
||||
state = state.copyWith(
|
||||
pages: json.map(
|
||||
(key, value) => MapEntry(
|
||||
key as P,
|
||||
key as PageType,
|
||||
jsonConfig!.fromJson(value),
|
||||
),
|
||||
),
|
||||
@@ -143,12 +147,15 @@ class InfiniteQuery<T, E, K, P>
|
||||
final _mutex = Mutex();
|
||||
final _box = Hive.lazyBox("cache");
|
||||
|
||||
List<T> get pages => state.pages.map((e) => e.data).whereType<T>().toList();
|
||||
List<E> get errors => state.pages.map((e) => e.error).whereType<E>().toList();
|
||||
P get lastPage => state.lastPage;
|
||||
List<DataType> get pages =>
|
||||
state.pages.map((e) => e.data).whereType<DataType>().toList();
|
||||
List<ErrorType> get errors =>
|
||||
state.pages.map((e) => e.error).whereType<ErrorType>().toList();
|
||||
PageType get lastPage => state.lastPage;
|
||||
|
||||
bool get isLoadingPage => !hasPageData && !hasPageError && _mutex.isLocked;
|
||||
bool get isRefreshingPage => (hasPageData || hasPageError) && _mutex.isLocked;
|
||||
bool get isInactive => !hasListeners;
|
||||
|
||||
bool get hasPages => pages.isNotEmpty;
|
||||
bool get hasErrors => errors.isNotEmpty;
|
||||
@@ -158,7 +165,7 @@ class InfiniteQuery<T, E, K, P>
|
||||
|
||||
bool get hasNextPage => state.hasNextPage;
|
||||
|
||||
Future<void> _operation(P page) {
|
||||
Future<void> _operation(PageType page) {
|
||||
return _mutex.protect(() async {
|
||||
retryOperation(
|
||||
() => state.queryFn(page),
|
||||
@@ -170,7 +177,8 @@ class InfiniteQuery<T, E, K, P>
|
||||
state.pages
|
||||
.firstWhere(
|
||||
(e) => e.page == page,
|
||||
orElse: () => InfiniteQueryPage<T, E, P>(
|
||||
orElse: () =>
|
||||
InfiniteQueryPage<DataType, ErrorType, PageType>(
|
||||
page: page,
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: refreshConfig.staleDuration,
|
||||
@@ -181,7 +189,7 @@ class InfiniteQuery<T, E, K, P>
|
||||
);
|
||||
if (jsonConfig != null) {
|
||||
await _box.put(
|
||||
key.value,
|
||||
key.toString(),
|
||||
state.pages.map(
|
||||
(e) => MapEntry(
|
||||
e.page,
|
||||
@@ -198,7 +206,8 @@ class InfiniteQuery<T, E, K, P>
|
||||
state.pages
|
||||
.firstWhere(
|
||||
(e) => e.page == page,
|
||||
orElse: () => InfiniteQueryPage<T, E, P>(
|
||||
orElse: () =>
|
||||
InfiniteQueryPage<DataType, ErrorType, PageType>(
|
||||
page: page,
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: refreshConfig.staleDuration,
|
||||
@@ -212,14 +221,14 @@ class InfiniteQuery<T, E, K, P>
|
||||
});
|
||||
}
|
||||
|
||||
Future<T?> fetch() async {
|
||||
Future<DataType?> fetch() async {
|
||||
final lastPage = state.lastPage;
|
||||
if (_mutex.isLocked || hasPageData || hasPageError)
|
||||
return state.pages.last.data;
|
||||
return await _operation(lastPage).then((_) => state.pages.last.data);
|
||||
}
|
||||
|
||||
Future<T?> refresh([P? page]) async {
|
||||
Future<DataType?> refresh([PageType? page]) async {
|
||||
page ??= lastPage;
|
||||
if (_mutex.isLocked)
|
||||
return state.pages.firstWhereOrNull((e) => e.page == page)?.data;
|
||||
@@ -228,14 +237,14 @@ class InfiniteQuery<T, E, K, P>
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<T>?> refreshAll() async {
|
||||
Future<List<DataType>?> refreshAll() async {
|
||||
if (_mutex.isLocked) return pages;
|
||||
return await Future.wait(
|
||||
state.pages.map((e) => _operation(e.page)),
|
||||
).then((_) => pages);
|
||||
}
|
||||
|
||||
Future<T?> fetchNext() async {
|
||||
Future<DataType?> fetchNext() async {
|
||||
final nextPage = state.getNextPage;
|
||||
if (_mutex.isLocked || nextPage != null) {
|
||||
return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data;
|
||||
@@ -244,4 +253,27 @@ class InfiniteQuery<T, E, K, P>
|
||||
return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
RemoveListener addListener(
|
||||
Listener<InfiniteQueryState<DataType, ErrorType, PageType>> listener, {
|
||||
bool fireImmediately = true,
|
||||
}) {
|
||||
if (refreshConfig.refreshOnMount) {
|
||||
refreshAll();
|
||||
} else {
|
||||
Future.wait(
|
||||
state.pages.map((page) async {
|
||||
if (page.isStale) {
|
||||
return await refresh(page.page);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return super.addListener(listener, fireImmediately: fireImmediately);
|
||||
}
|
||||
|
||||
@override
|
||||
operator ==(Object other) =>
|
||||
identical(this, other) || other is InfiniteQuery && key == other.key;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import 'package:fl_query/src/collections/json_config.dart';
|
||||
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
import 'package:fl_query/src/core/retryer.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart' hide Listener;
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:state_notifier/state_notifier.dart';
|
||||
|
||||
typedef QueryFn<T> = FutureOr<T?> Function();
|
||||
typedef QueryFn<DataType> = FutureOr<DataType?> Function();
|
||||
|
||||
class QueryState<T, E> {
|
||||
final T? data;
|
||||
final E? error;
|
||||
final QueryFn<T> queryFn;
|
||||
class QueryState<DataType, ErrorType> {
|
||||
final DataType? data;
|
||||
final ErrorType? error;
|
||||
final QueryFn<DataType> queryFn;
|
||||
|
||||
final DateTime updatedAt;
|
||||
final Duration staleDuration;
|
||||
@@ -32,12 +32,12 @@ class QueryState<T, E> {
|
||||
return DateTime.now().difference(updatedAt) > staleDuration;
|
||||
}
|
||||
|
||||
QueryState<T, E> copyWith({
|
||||
T? data,
|
||||
E? error,
|
||||
QueryFn<T>? queryFn,
|
||||
QueryState<DataType, ErrorType> copyWith({
|
||||
DataType? data,
|
||||
ErrorType? error,
|
||||
QueryFn<DataType>? queryFn,
|
||||
}) {
|
||||
return QueryState<T, E>(
|
||||
return QueryState<DataType, ErrorType>(
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: staleDuration,
|
||||
data: data ?? this.data,
|
||||
@@ -47,23 +47,26 @@ class QueryState<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
||||
with Retryer<T, E> {
|
||||
final ValueKey<K> key;
|
||||
final T? initial;
|
||||
class Query<DataType, ErrorType, KeyType>
|
||||
extends StateNotifier<QueryState<DataType, ErrorType>>
|
||||
with Retryer<DataType, ErrorType> {
|
||||
final ValueKey<KeyType> key;
|
||||
final DataType? initial;
|
||||
|
||||
final RefreshConfig refreshConfig;
|
||||
final RetryConfig retryConfig;
|
||||
final JsonConfig<T>? jsonConfig;
|
||||
final JsonConfig<DataType>? jsonConfig;
|
||||
Query(
|
||||
this.key,
|
||||
QueryFn<T> queryFn, {
|
||||
QueryFn<DataType> queryFn, {
|
||||
this.initial,
|
||||
this.retryConfig = DefaultConstants.retryConfig,
|
||||
this.refreshConfig = DefaultConstants.refreshConfig,
|
||||
this.jsonConfig,
|
||||
}) : _box = Hive.lazyBox("cache"),
|
||||
super(QueryState<T, E>(
|
||||
_dataController = StreamController<DataType>.broadcast(),
|
||||
_errorController = StreamController<ErrorType>.broadcast(),
|
||||
super(QueryState<DataType, ErrorType>(
|
||||
updatedAt: DateTime.now(),
|
||||
staleDuration: refreshConfig.staleDuration,
|
||||
data: initial,
|
||||
@@ -71,9 +74,13 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
||||
)) {
|
||||
if (jsonConfig != null) {
|
||||
_mutex.protect(() async {
|
||||
final json = await _box.get(key);
|
||||
final json = await _box.get(key.toString());
|
||||
if (json != null) {
|
||||
state = state.copyWith(data: jsonConfig!.fromJson(json));
|
||||
state = state.copyWith(
|
||||
data: jsonConfig!.fromJson(
|
||||
Map.castFrom<dynamic, dynamic, String, dynamic>(json),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,53 +94,86 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
||||
|
||||
final LazyBox _box;
|
||||
final _mutex = Mutex();
|
||||
final StreamController<DataType> _dataController;
|
||||
final StreamController<ErrorType> _errorController;
|
||||
|
||||
bool get isInitial => state.data == initial;
|
||||
bool get isLoading => isInitial ? _mutex.isLocked : !hasData && !hasError;
|
||||
bool get isRefreshing =>
|
||||
((!isInitial && hasData) || hasError) && _mutex.isLocked;
|
||||
bool get isInactive => !hasListeners;
|
||||
bool get hasData => state.data != null;
|
||||
bool get hasError => state.error != null;
|
||||
|
||||
T? get data => state.data;
|
||||
E? get error => state.error;
|
||||
DataType? get data => state.data;
|
||||
ErrorType? get error => state.error;
|
||||
Stream<DataType> get dataStream => _dataController.stream;
|
||||
Stream<ErrorType> get errorStream => _errorController.stream;
|
||||
|
||||
Future<void> _operate() {
|
||||
return _mutex.protect(() async {
|
||||
retryOperation(
|
||||
state.queryFn,
|
||||
config: retryConfig,
|
||||
onSuccessful: (T? data) {
|
||||
onSuccessful: (DataType? data) {
|
||||
state = state.copyWith(data: data);
|
||||
if (jsonConfig != null) {
|
||||
if (data != null) _dataController.add(data);
|
||||
if (jsonConfig != null && data != null) {
|
||||
_box.put(
|
||||
key,
|
||||
jsonConfig!.toJson(data!),
|
||||
key.toString(),
|
||||
jsonConfig!.toJson(data),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFailed: (E? error) {
|
||||
onFailed: (ErrorType? error) {
|
||||
state = state.copyWith(error: error);
|
||||
if (error != null) _errorController.add(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<T?> fetch() async {
|
||||
Future<DataType?> fetch() async {
|
||||
if (_mutex.isLocked || hasData || hasError) return state.data;
|
||||
return _operate().then((_) => state.data);
|
||||
}
|
||||
|
||||
Future<T?> refresh() async {
|
||||
Future<DataType?> refresh() async {
|
||||
if (_mutex.isLocked) return state.data;
|
||||
return _operate().then((_) => state.data);
|
||||
}
|
||||
|
||||
void updateQueryFn(QueryFn<T> queryFn) {
|
||||
void updateQueryFn(QueryFn<DataType> queryFn) {
|
||||
state = state.copyWith(queryFn: queryFn);
|
||||
if (state.isStale || refreshConfig.refreshOnQueryFnChange) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void setData(T data) {
|
||||
void setData(DataType data) {
|
||||
state = state.copyWith(data: data);
|
||||
}
|
||||
|
||||
@override
|
||||
RemoveListener addListener(Listener<QueryState<DataType, ErrorType>> listener,
|
||||
{bool fireImmediately = true}) {
|
||||
if (state.isStale || refreshConfig.refreshOnMount) {
|
||||
refresh();
|
||||
}
|
||||
return super.addListener(listener, fireImmediately: fireImmediately);
|
||||
}
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is Query && key.value == other.key.value);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode;
|
||||
|
||||
Query<NewDataType, NewErrorType, NewKeyType>
|
||||
cast<NewDataType, NewErrorType, NewKeyType>() {
|
||||
return this as Query<NewDataType, NewErrorType, NewKeyType>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
mixin Retryer<T, E> {
|
||||
VoidCallback retryOperation(
|
||||
void retryOperation(
|
||||
FutureOr<T?> Function() operation, {
|
||||
required RetryConfig config,
|
||||
required Function(T?) onSuccessful,
|
||||
required Function(E?) onFailed,
|
||||
}) {
|
||||
int retries = 0;
|
||||
late Timer timer;
|
||||
VoidCallback? cancel;
|
||||
|
||||
void retry() {
|
||||
if (retries < config.maxRetries) {
|
||||
retries++;
|
||||
timer = Timer(config.retryDelay, () async {
|
||||
await Future.value(operation()).then((T? data) {
|
||||
onSuccessful(data);
|
||||
}).catchError((error) {
|
||||
if (error is E) onFailed(error);
|
||||
retry();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cancel?.call();
|
||||
required void Function(T?) onSuccessful,
|
||||
required void Function(E?) onFailed,
|
||||
}) async {
|
||||
for (int attempts = 0; attempts < config.maxRetries; attempts++) {
|
||||
final completer = Completer<T?>();
|
||||
await Future.delayed(
|
||||
attempts == 0 ? Duration.zero : config.retryDelay,
|
||||
operation,
|
||||
).then(completer.complete).catchError(completer.completeError);
|
||||
await Future.delayed(config.timeout, () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(
|
||||
TimeoutException(
|
||||
'Operation timed out after ${config.timeout.inSeconds} seconds',
|
||||
),
|
||||
StackTrace.current,
|
||||
);
|
||||
}
|
||||
});
|
||||
try {
|
||||
final result = await completer.future;
|
||||
onSuccessful(result);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (attempts == config.maxRetries - 1 && e is E?) {
|
||||
onFailed(e as E?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancel = timer.cancel;
|
||||
|
||||
timer = Timer(config.timeout, retry);
|
||||
|
||||
return cancel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/collections/default_configs.dart';
|
||||
import 'package:fl_query/src/collections/json_config.dart';
|
||||
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
import 'package:fl_query/src/core/client.dart';
|
||||
import 'package:fl_query/src/core/query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef QueryBuilderFn<DataType, ErrorType, KeyType> = Widget Function(
|
||||
BuildContext context,
|
||||
Query<DataType, ErrorType, KeyType> query,
|
||||
);
|
||||
|
||||
class QueryBuilder<DataType, ErrorType, KeyType> extends StatefulWidget {
|
||||
final QueryFn<DataType> queryFn;
|
||||
final ValueKey<KeyType> queryKey;
|
||||
|
||||
final QueryBuilderFn builder;
|
||||
final DataType? initial;
|
||||
|
||||
final RetryConfig retryConfig;
|
||||
final RefreshConfig refreshConfig;
|
||||
final JsonConfig<DataType>? jsonConfig;
|
||||
|
||||
final ValueChanged<DataType>? onData;
|
||||
final ValueChanged<ErrorType>? onError;
|
||||
|
||||
// widget specific
|
||||
final bool enabled;
|
||||
|
||||
const QueryBuilder(
|
||||
this.queryKey,
|
||||
this.queryFn, {
|
||||
required this.builder,
|
||||
this.initial,
|
||||
this.retryConfig = DefaultConstants.retryConfig,
|
||||
this.refreshConfig = DefaultConstants.refreshConfig,
|
||||
this.jsonConfig,
|
||||
this.onData,
|
||||
this.onError,
|
||||
this.enabled = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<QueryBuilder<DataType, ErrorType, KeyType>> createState() =>
|
||||
_QueryBuilderState<DataType, ErrorType, KeyType>();
|
||||
}
|
||||
|
||||
class _QueryBuilderState<DataType, ErrorType, KeyType>
|
||||
extends State<QueryBuilder<DataType, ErrorType, KeyType>> {
|
||||
Query<DataType, ErrorType, KeyType>? query;
|
||||
|
||||
VoidCallback? removeListener;
|
||||
|
||||
StreamSubscription<DataType>? dataSubscription;
|
||||
StreamSubscription<ErrorType>? errorSubscription;
|
||||
|
||||
void update(_) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
setState(() {
|
||||
query = QueryClient.of(context).createQuery(
|
||||
widget.queryKey,
|
||||
widget.queryFn,
|
||||
initial: widget.initial,
|
||||
retryConfig: widget.retryConfig,
|
||||
refreshConfig: widget.refreshConfig,
|
||||
jsonConfig: widget.jsonConfig,
|
||||
);
|
||||
|
||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||
|
||||
removeListener = query!.addListener(update);
|
||||
});
|
||||
if (widget.enabled) {
|
||||
await query!.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await initialize();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
dataSubscription?.cancel();
|
||||
errorSubscription?.cancel();
|
||||
removeListener?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(
|
||||
QueryBuilder<DataType, ErrorType, KeyType> oldWidget,
|
||||
) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.queryKey != widget.queryKey) {
|
||||
dataSubscription?.cancel();
|
||||
errorSubscription?.cancel();
|
||||
removeListener?.call();
|
||||
initialize();
|
||||
return;
|
||||
}
|
||||
if (oldWidget.queryFn != widget.queryFn) {
|
||||
query!.updateQueryFn(widget.queryFn);
|
||||
}
|
||||
if (oldWidget.onData != widget.onData) {
|
||||
dataSubscription?.cancel();
|
||||
dataSubscription = query!.dataStream.listen(widget.onData);
|
||||
}
|
||||
if (oldWidget.onError != widget.onError) {
|
||||
errorSubscription?.cancel();
|
||||
errorSubscription = query!.errorStream.listen(widget.onError);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (query == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return widget.builder(context, query!);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user