feat: infinite query implementation
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import 'package:fl_query/src/core/query.dart';
|
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||||
import 'package:fl_query/src/core/retryer.dart';
|
import 'package:fl_query/src/collections/retry_config.dart';
|
||||||
|
|
||||||
abstract class DefaultConstants {
|
abstract class DefaultConstants {
|
||||||
static const RetryConfig retryConfig = RetryConfig(
|
static const RetryConfig retryConfig = RetryConfig(
|
||||||
@@ -11,5 +11,6 @@ abstract class DefaultConstants {
|
|||||||
static const RefreshConfig refreshConfig = RefreshConfig(
|
static const RefreshConfig refreshConfig = RefreshConfig(
|
||||||
staleDuration: Duration(seconds: 10),
|
staleDuration: Duration(seconds: 10),
|
||||||
refreshInterval: Duration(seconds: 5),
|
refreshInterval: Duration(seconds: 5),
|
||||||
|
refreshOnMount: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class JsonConfig<T> {
|
||||||
|
final Map<String, dynamic> Function(T data) toJson;
|
||||||
|
final T Function(Map<String, dynamic> json) fromJson;
|
||||||
|
|
||||||
|
const JsonConfig({
|
||||||
|
required this.toJson,
|
||||||
|
required this.fromJson,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class RefreshConfig {
|
||||||
|
final Duration staleDuration;
|
||||||
|
final Duration refreshInterval;
|
||||||
|
final bool refreshOnMount;
|
||||||
|
|
||||||
|
const RefreshConfig({
|
||||||
|
required this.staleDuration,
|
||||||
|
required this.refreshInterval,
|
||||||
|
required this.refreshOnMount,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class RetryConfig {
|
||||||
|
final int maxRetries;
|
||||||
|
final Duration retryDelay;
|
||||||
|
final Duration timeout;
|
||||||
|
|
||||||
|
const RetryConfig({
|
||||||
|
required this.maxRetries,
|
||||||
|
required this.retryDelay,
|
||||||
|
required this.timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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/retryer.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
class InfiniteQueryPage<T, E, P> {
|
||||||
|
final P page;
|
||||||
|
final T? data;
|
||||||
|
final E? error;
|
||||||
|
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final Duration staleDuration;
|
||||||
|
|
||||||
|
const InfiniteQueryPage({
|
||||||
|
required this.page,
|
||||||
|
this.data,
|
||||||
|
this.error,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.staleDuration,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isStale => DateTime.now().difference(updatedAt) > staleDuration;
|
||||||
|
|
||||||
|
InfiniteQueryPage<T, E, P> copyWith({
|
||||||
|
T? data,
|
||||||
|
E? error,
|
||||||
|
}) {
|
||||||
|
return InfiniteQueryPage<T, E, P>(
|
||||||
|
page: page,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
staleDuration: staleDuration,
|
||||||
|
data: data ?? this.data,
|
||||||
|
error: error ?? this.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is InfiniteQueryPage<T, E, P> && 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;
|
||||||
|
|
||||||
|
const InfiniteQueryState({
|
||||||
|
required this.pages,
|
||||||
|
required this.queryFn,
|
||||||
|
required this.nextPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
P get lastPage => pages.last.page;
|
||||||
|
P? 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,
|
||||||
|
}) {
|
||||||
|
return InfiniteQueryState<T, E, P>(
|
||||||
|
pages: pages ?? this.pages,
|
||||||
|
queryFn: queryFn ?? this.queryFn,
|
||||||
|
nextPage: nextPage ?? this.nextPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InfiniteQuery<T, E, K, P>
|
||||||
|
extends StateNotifier<InfiniteQueryState<T, E, P>> with Retryer<T, E> {
|
||||||
|
final ValueKey<K> key;
|
||||||
|
final RetryConfig retryConfig;
|
||||||
|
final RefreshConfig refreshConfig;
|
||||||
|
final JsonConfig<T>? jsonConfig;
|
||||||
|
|
||||||
|
InfiniteQuery(
|
||||||
|
this.key,
|
||||||
|
InfiniteQueryFn<T, P> queryFn, {
|
||||||
|
required InfiniteQueryNextPage<T, P> nextPage,
|
||||||
|
required P initialParam,
|
||||||
|
this.retryConfig = DefaultConstants.retryConfig,
|
||||||
|
this.refreshConfig = DefaultConstants.refreshConfig,
|
||||||
|
this.jsonConfig,
|
||||||
|
}) : super(InfiniteQueryState<T, E, P>(
|
||||||
|
pages: {
|
||||||
|
InfiniteQueryPage<T, E, P>(
|
||||||
|
page: initialParam,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
staleDuration: refreshConfig.staleDuration,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
queryFn: queryFn,
|
||||||
|
nextPage: nextPage,
|
||||||
|
)) {
|
||||||
|
if (jsonConfig != null) {
|
||||||
|
_mutex.protect(() async {
|
||||||
|
final json = await _box.get(key.value);
|
||||||
|
if (json != null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
pages: json.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
key as P,
|
||||||
|
jsonConfig!.fromJson(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Timer.periodic(refreshConfig.refreshInterval, (_) async {
|
||||||
|
await Future.wait(
|
||||||
|
state.pages.map((page) async {
|
||||||
|
if (page.isStale) {
|
||||||
|
return await refresh(page.page);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
bool get isLoadingPage => !hasPageData && !hasPageError && _mutex.isLocked;
|
||||||
|
bool get isRefreshingPage => (hasPageData || hasPageError) && _mutex.isLocked;
|
||||||
|
|
||||||
|
bool get hasPages => pages.isNotEmpty;
|
||||||
|
bool get hasErrors => errors.isNotEmpty;
|
||||||
|
|
||||||
|
bool get hasPageData => state.pages.last.data != null;
|
||||||
|
bool get hasPageError => state.pages.last.error != null;
|
||||||
|
|
||||||
|
bool get hasNextPage => state.hasNextPage;
|
||||||
|
|
||||||
|
Future<void> _operation(P page) {
|
||||||
|
return _mutex.protect(() async {
|
||||||
|
retryOperation(
|
||||||
|
() => state.queryFn(page),
|
||||||
|
config: retryConfig,
|
||||||
|
onSuccessful: (data) async {
|
||||||
|
state = state.copyWith(
|
||||||
|
pages: {
|
||||||
|
...state.pages,
|
||||||
|
state.pages
|
||||||
|
.firstWhere(
|
||||||
|
(e) => e.page == page,
|
||||||
|
orElse: () => InfiniteQueryPage<T, E, P>(
|
||||||
|
page: page,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
staleDuration: refreshConfig.staleDuration,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.copyWith(data: data),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (jsonConfig != null) {
|
||||||
|
await _box.put(
|
||||||
|
key.value,
|
||||||
|
state.pages.map(
|
||||||
|
(e) => MapEntry(
|
||||||
|
e.page,
|
||||||
|
e.data != null ? jsonConfig!.toJson(e.data!) : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailed: (error) {
|
||||||
|
state = state.copyWith(
|
||||||
|
pages: {
|
||||||
|
...state.pages,
|
||||||
|
state.pages
|
||||||
|
.firstWhere(
|
||||||
|
(e) => e.page == page,
|
||||||
|
orElse: () => InfiniteQueryPage<T, E, P>(
|
||||||
|
page: page,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
staleDuration: refreshConfig.staleDuration,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.copyWith(error: error),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> 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 {
|
||||||
|
page ??= lastPage;
|
||||||
|
if (_mutex.isLocked)
|
||||||
|
return state.pages.firstWhereOrNull((e) => e.page == page)?.data;
|
||||||
|
return await _operation(page!).then((_) {
|
||||||
|
return state.pages.firstWhereOrNull((e) => e.page == page)?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>?> refreshAll() async {
|
||||||
|
if (_mutex.isLocked) return pages;
|
||||||
|
return await Future.wait(
|
||||||
|
state.pages.map((e) => _operation(e.page)),
|
||||||
|
).then((_) => pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> fetchNext() async {
|
||||||
|
final nextPage = state.getNextPage;
|
||||||
|
if (_mutex.isLocked || nextPage != null) {
|
||||||
|
return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data;
|
||||||
|
}
|
||||||
|
return await _operation(nextPage!).then((_) {
|
||||||
|
return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fl_query/src/collections/default_configs.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/retryer.dart';
|
import 'package:fl_query/src/core/retryer.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
@@ -12,7 +15,7 @@ typedef QueryFn<T> = FutureOr<T?> Function();
|
|||||||
class QueryState<T, E> {
|
class QueryState<T, E> {
|
||||||
final T? data;
|
final T? data;
|
||||||
final E? error;
|
final E? error;
|
||||||
final QueryFn<T>? queryFn;
|
final QueryFn<T> queryFn;
|
||||||
|
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final Duration staleDuration;
|
final Duration staleDuration;
|
||||||
@@ -20,7 +23,7 @@ class QueryState<T, E> {
|
|||||||
const QueryState({
|
const QueryState({
|
||||||
this.data,
|
this.data,
|
||||||
this.error,
|
this.error,
|
||||||
this.queryFn,
|
required this.queryFn,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.staleDuration,
|
required this.staleDuration,
|
||||||
});
|
});
|
||||||
@@ -44,26 +47,6 @@ class QueryState<T, E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsonConfig<T> {
|
|
||||||
final Map<String, dynamic> Function(T data) toJson;
|
|
||||||
final T Function(Map<String, dynamic> json) fromJson;
|
|
||||||
|
|
||||||
const JsonConfig({
|
|
||||||
required this.toJson,
|
|
||||||
required this.fromJson,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class RefreshConfig {
|
|
||||||
final Duration staleDuration;
|
|
||||||
final Duration refreshInterval;
|
|
||||||
|
|
||||||
const RefreshConfig({
|
|
||||||
required this.staleDuration,
|
|
||||||
required this.refreshInterval,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
||||||
with Retryer<T, E> {
|
with Retryer<T, E> {
|
||||||
final ValueKey<K> key;
|
final ValueKey<K> key;
|
||||||
@@ -112,10 +95,13 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
|||||||
bool get hasData => state.data != null;
|
bool get hasData => state.data != null;
|
||||||
bool get hasError => state.error != null;
|
bool get hasError => state.error != null;
|
||||||
|
|
||||||
|
T? get data => state.data;
|
||||||
|
E? get error => state.error;
|
||||||
|
|
||||||
Future<void> _operate() {
|
Future<void> _operate() {
|
||||||
return _mutex.protect(() async {
|
return _mutex.protect(() async {
|
||||||
retryOperation(
|
retryOperation(
|
||||||
state.queryFn!,
|
state.queryFn,
|
||||||
config: retryConfig,
|
config: retryConfig,
|
||||||
onSuccessful: (T? data) {
|
onSuccessful: (T? data) {
|
||||||
state = state.copyWith(data: data);
|
state = state.copyWith(data: data);
|
||||||
@@ -134,7 +120,7 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<T?> fetch() async {
|
Future<T?> fetch() async {
|
||||||
if (_mutex.isLocked || hasData) return state.data;
|
if (_mutex.isLocked || hasData || hasError) return state.data;
|
||||||
return _operate().then((_) => state.data);
|
return _operate().then((_) => state.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,4 +128,12 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
|||||||
if (_mutex.isLocked) return state.data;
|
if (_mutex.isLocked) return state.data;
|
||||||
return _operate().then((_) => state.data);
|
return _operate().then((_) => state.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateQueryFn(QueryFn<T> queryFn) {
|
||||||
|
state = state.copyWith(queryFn: queryFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setData(T data) {
|
||||||
|
state = state.copyWith(data: data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_query/src/collections/retry_config.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class RetryConfig {
|
|
||||||
final int maxRetries;
|
|
||||||
final Duration retryDelay;
|
|
||||||
final Duration timeout;
|
|
||||||
|
|
||||||
const RetryConfig({
|
|
||||||
required this.maxRetries,
|
|
||||||
required this.retryDelay,
|
|
||||||
required this.timeout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin Retryer<T, E> {
|
mixin Retryer<T, E> {
|
||||||
VoidCallback retryOperation(
|
VoidCallback retryOperation(
|
||||||
FutureOr<T?> Function() operation, {
|
FutureOr<T?> Function() operation, {
|
||||||
|
|||||||
Reference in New Issue
Block a user