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/core/retryer.dart';
|
||||
import 'package:fl_query/src/collections/refresh_config.dart';
|
||||
import 'package:fl_query/src/collections/retry_config.dart';
|
||||
|
||||
abstract class DefaultConstants {
|
||||
static const RetryConfig retryConfig = RetryConfig(
|
||||
@@ -11,5 +11,6 @@ abstract class DefaultConstants {
|
||||
static const RefreshConfig refreshConfig = RefreshConfig(
|
||||
staleDuration: Duration(seconds: 10),
|
||||
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 '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/cupertino.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
@@ -12,7 +15,7 @@ typedef QueryFn<T> = FutureOr<T?> Function();
|
||||
class QueryState<T, E> {
|
||||
final T? data;
|
||||
final E? error;
|
||||
final QueryFn<T>? queryFn;
|
||||
final QueryFn<T> queryFn;
|
||||
|
||||
final DateTime updatedAt;
|
||||
final Duration staleDuration;
|
||||
@@ -20,7 +23,7 @@ class QueryState<T, E> {
|
||||
const QueryState({
|
||||
this.data,
|
||||
this.error,
|
||||
this.queryFn,
|
||||
required this.queryFn,
|
||||
required this.updatedAt,
|
||||
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>>
|
||||
with Retryer<T, E> {
|
||||
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 hasError => state.error != null;
|
||||
|
||||
T? get data => state.data;
|
||||
E? get error => state.error;
|
||||
|
||||
Future<void> _operate() {
|
||||
return _mutex.protect(() async {
|
||||
retryOperation(
|
||||
state.queryFn!,
|
||||
state.queryFn,
|
||||
config: retryConfig,
|
||||
onSuccessful: (T? data) {
|
||||
state = state.copyWith(data: data);
|
||||
@@ -134,7 +120,7 @@ class Query<T, E, K> extends StateNotifier<QueryState<T, E>>
|
||||
}
|
||||
|
||||
Future<T?> fetch() async {
|
||||
if (_mutex.isLocked || hasData) return state.data;
|
||||
if (_mutex.isLocked || hasData || hasError) return 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;
|
||||
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 'package:fl_query/src/collections/retry_config.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> {
|
||||
VoidCallback retryOperation(
|
||||
FutureOr<T?> Function() operation, {
|
||||
|
||||
Reference in New Issue
Block a user