cache time & garbage collection support added
onData & onError callbacks WIP
This commit is contained in:
@@ -7,7 +7,11 @@ class AnotherComponent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final lol = QueryBowl.of(context).getQuery<String, void>("greetings");
|
||||
final deadQuery =
|
||||
QueryBowl.of(context).getQuery<String, String>("external_data");
|
||||
if (lol?.data == null) return const CircularProgressIndicator();
|
||||
return Text("${lol!.data!} from AnotherComponent");
|
||||
return Text(
|
||||
"${lol!.data!} from AnotherComponent\nDeadQuery (It should be null after 10 seconds): ${deadQuery?.data}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,18 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
|
||||
final successJob = QueryJob<String, void>(
|
||||
queryKey: "greetings",
|
||||
task: (queryKey, _) => Future.delayed(const Duration(seconds: 2),
|
||||
() => "Welcome ($queryKey) ${Random.secure().nextInt(100)}"));
|
||||
queryKey: "greetings",
|
||||
task: (queryKey, _) => Future.delayed(const Duration(seconds: 2),
|
||||
() => "Welcome ($queryKey) ${Random.secure().nextInt(100)}"),
|
||||
);
|
||||
|
||||
final failedJob = QueryJob<String, void>(
|
||||
queryKey: "failure",
|
||||
task: (queryKey, _) => Random().nextBool()
|
||||
? Future.error("[$queryKey] Failed for unknown reason")
|
||||
: Future.value("Success, you'll get slowly ${Random().nextInt(100)}!"),
|
||||
: Future.value(
|
||||
"Success, you'll get slowly ${Random().nextInt(100)}!",
|
||||
),
|
||||
);
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
@@ -97,7 +100,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
children: [
|
||||
if (query.hasError)
|
||||
Text(
|
||||
"${query.error}. Retrying: ${query.retryAttempts}"),
|
||||
"${query.error}. Retrying: ${query.retryAttempts}",
|
||||
),
|
||||
if (query.hasData)
|
||||
Text(
|
||||
"Success after ${query.retryAttempts}. Data: ${query.data}"),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
final jobWithExternalData = QueryJob<String, String>(
|
||||
queryKey: "external_data",
|
||||
cacheTime: const Duration(seconds: 10),
|
||||
task: (queryKey, data) {
|
||||
return Future.delayed(const Duration(milliseconds: 500),
|
||||
() => "Hello from $queryKey with $data");
|
||||
|
||||
@@ -14,9 +14,8 @@ class QueryJob<T extends Object, Outside> {
|
||||
|
||||
// got from global options
|
||||
final Duration? staleTime;
|
||||
final Duration? cacheTime;
|
||||
|
||||
final QueryListener<T>? onData;
|
||||
final QueryListener<dynamic>? onError;
|
||||
QueryJob({
|
||||
required this.queryKey,
|
||||
required this.task,
|
||||
@@ -24,8 +23,7 @@ class QueryJob<T extends Object, Outside> {
|
||||
this.retryDelay,
|
||||
this.initialData,
|
||||
this.staleTime,
|
||||
this.onData,
|
||||
this.onError,
|
||||
this.cacheTime,
|
||||
this.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
|
||||
// got from global options
|
||||
final Duration _staleTime;
|
||||
final Duration _cacheTime;
|
||||
|
||||
// all properties
|
||||
T? data;
|
||||
@@ -54,10 +55,16 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
|
||||
Outside? _prevUsedExternalData;
|
||||
|
||||
/// used for keeping track of query activity. If the are no mounts &
|
||||
/// the passed cached time is over than the query is removed from
|
||||
/// storage/cache
|
||||
Set<Widget> _mounts = {};
|
||||
|
||||
Query({
|
||||
required this.queryKey,
|
||||
required this.task,
|
||||
required Duration staleTime,
|
||||
required Duration cacheTime,
|
||||
required Outside externalData,
|
||||
required this.retries,
|
||||
required this.retryDelay,
|
||||
@@ -67,6 +74,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
QueryListener<dynamic>? onError,
|
||||
}) : status = QueryStatus.pending,
|
||||
_staleTime = staleTime,
|
||||
_cacheTime = cacheTime,
|
||||
_initialData = initialData,
|
||||
_externalData = externalData,
|
||||
data = initialData,
|
||||
@@ -74,18 +82,22 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
_onError = onError,
|
||||
updatedAt = DateTime.now();
|
||||
|
||||
Query.fromOptions(QueryJob<T, Outside> options,
|
||||
{required Outside externalData})
|
||||
: queryKey = options.queryKey,
|
||||
Query.fromOptions(
|
||||
QueryJob<T, Outside> options, {
|
||||
required Outside externalData,
|
||||
QueryListener<T>? onData,
|
||||
QueryListener<dynamic>? onError,
|
||||
}) : queryKey = options.queryKey,
|
||||
enabled = options.enabled ?? true,
|
||||
task = options.task,
|
||||
retries = options.retries ?? 3,
|
||||
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200),
|
||||
_staleTime = options.staleTime ?? const Duration(days: 1),
|
||||
_staleTime = options.staleTime ?? const Duration(milliseconds: 500),
|
||||
_cacheTime = options.cacheTime ?? const Duration(minutes: 5),
|
||||
_initialData = options.initialData,
|
||||
_externalData = externalData,
|
||||
_onData = options.onData,
|
||||
_onError = options.onError,
|
||||
_onData = onData,
|
||||
_onError = onError,
|
||||
data = options.initialData,
|
||||
status = QueryStatus.pending,
|
||||
updatedAt = DateTime.now();
|
||||
@@ -100,11 +112,29 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
status == QueryStatus.refetching && (data != null || error != null);
|
||||
bool get isSucceeded => status == QueryStatus.succeed && data != null;
|
||||
bool get isIdle => isSucceeded && error == null;
|
||||
bool get isInactive => _mounts.isEmpty;
|
||||
Outside get externalData => _externalData;
|
||||
Outside? get prevUsedExternalData => _prevUsedExternalData;
|
||||
|
||||
// all methods
|
||||
|
||||
void mount(Widget widget) {
|
||||
_mounts.add(widget);
|
||||
}
|
||||
|
||||
void unmount(Widget widget) {
|
||||
if (_mounts.length == 1) {
|
||||
Future.delayed(_cacheTime, () {
|
||||
_mounts.remove(widget);
|
||||
// for letting know QueryBowl that this one's time has come for
|
||||
// getting crushed
|
||||
notifyListeners();
|
||||
});
|
||||
} else {
|
||||
_mounts.remove(widget);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the task function & doesn't check if there's already
|
||||
/// cached data available
|
||||
Future<void> _execute() async {
|
||||
@@ -213,4 +243,11 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
|
||||
}
|
||||
|
||||
A? cast<A>() => this is A ? this as A : null;
|
||||
|
||||
String get debugLabel => "Query($queryKey)";
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return debugLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@ import 'package:flutter/widgets.dart';
|
||||
class QueryBowlScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration staleTime;
|
||||
final Duration cacheTime;
|
||||
|
||||
/// used for periodically checking if any query got stale.
|
||||
/// If none is supplied then half of the value of staleTime is used
|
||||
final Duration? refreshInterval;
|
||||
final Duration refreshInterval;
|
||||
const QueryBowlScope({
|
||||
required this.child,
|
||||
this.staleTime = const Duration(minutes: 5),
|
||||
this.refreshInterval,
|
||||
this.staleTime = const Duration(milliseconds: 500),
|
||||
this.cacheTime = const Duration(minutes: 5),
|
||||
this.refreshInterval = const Duration(minutes: 5),
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -33,10 +35,7 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
|
||||
super.initState();
|
||||
queries = {};
|
||||
refreshIntervalTimer = Timer.periodic(
|
||||
widget.refreshInterval ??
|
||||
Duration(
|
||||
milliseconds: (widget.staleTime.inMilliseconds / 2).round(),
|
||||
),
|
||||
widget.refreshInterval,
|
||||
_checkAndUpdateStaleQueriesOnBg,
|
||||
);
|
||||
}
|
||||
@@ -58,19 +57,25 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
|
||||
|
||||
void _listenToQueryUpdate() {
|
||||
for (final query in queries) {
|
||||
query.addListener(updateQueries);
|
||||
query.addListener(() => updateQueries(query));
|
||||
}
|
||||
}
|
||||
|
||||
void _disposeListeners() {
|
||||
for (final query in queries) {
|
||||
query.removeListener(updateQueries);
|
||||
query.removeListener(() => updateQueries(query));
|
||||
}
|
||||
}
|
||||
|
||||
void updateQueries() {
|
||||
void updateQueries(Query query) {
|
||||
setState(() {
|
||||
queries = Set.from(queries);
|
||||
// checking & not including inactive queries
|
||||
// basically garbage collecting queries
|
||||
queries = Set.from(
|
||||
query.isInactive
|
||||
? queries.where((el) => el.queryKey != query.queryKey)
|
||||
: queries,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,8 +119,13 @@ class QueryBowl extends InheritedWidget {
|
||||
_queries = queries,
|
||||
super(child: child, key: key);
|
||||
|
||||
Future<T?> fetchQuery<T extends Object, Outside>(QueryJob<T, Outside> options,
|
||||
{required Outside externalData}) async {
|
||||
Future<T?> fetchQuery<T extends Object, Outside>(
|
||||
QueryJob<T, Outside> options, {
|
||||
required Outside externalData,
|
||||
final QueryListener<T>? onData,
|
||||
final QueryListener<dynamic>? onError,
|
||||
Widget? mount,
|
||||
}) async {
|
||||
final prevQuery =
|
||||
_queries.firstWhereOrNull((q) => q.queryKey == options.queryKey);
|
||||
if (prevQuery is Query<T, Outside>) {
|
||||
@@ -123,16 +133,23 @@ class QueryBowl extends InheritedWidget {
|
||||
// changed
|
||||
final hasExternalDataChanged =
|
||||
prevQuery.prevUsedExternalData != externalData;
|
||||
if (mount != null) prevQuery.mount(mount);
|
||||
if (!prevQuery.hasData || hasExternalDataChanged) {
|
||||
if (hasExternalDataChanged) prevQuery.setExternalData(externalData);
|
||||
return prevQuery.fetched
|
||||
? await prevQuery.refetch()
|
||||
: await prevQuery.fetch();
|
||||
}
|
||||
// mounting the widget that is using the query in the prevQuery
|
||||
return prevQuery.data;
|
||||
}
|
||||
final query =
|
||||
Query<T, Outside>.fromOptions(options, externalData: externalData);
|
||||
final query = Query<T, Outside>.fromOptions(
|
||||
options,
|
||||
externalData: externalData,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
);
|
||||
if (mount != null) query.mount(mount);
|
||||
_addQuery<T, Outside>(query);
|
||||
return await query.fetch();
|
||||
}
|
||||
|
||||
@@ -8,10 +8,19 @@ class QueryBuilder<T extends Object, Outside> extends StatefulWidget {
|
||||
final QueryJob<T, Outside> job;
|
||||
final Outside externalData;
|
||||
|
||||
/// Called when the query returns new data, on query
|
||||
/// refetch or query gets expired
|
||||
final QueryListener<T>? onData;
|
||||
|
||||
/// Called when the query returns error
|
||||
final QueryListener<dynamic>? onError;
|
||||
|
||||
const QueryBuilder({
|
||||
required this.job,
|
||||
required this.externalData,
|
||||
required this.builder,
|
||||
this.onData,
|
||||
this.onError,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -22,28 +31,51 @@ class QueryBuilder<T extends Object, Outside> extends StatefulWidget {
|
||||
|
||||
class _QueryBuilderState<T extends Object, Outside>
|
||||
extends State<QueryBuilder<T, Outside>> {
|
||||
late QueryBowl queryBowl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await QueryBowl.of(context).fetchQuery<T, Outside>(widget.job,
|
||||
externalData: widget.externalData);
|
||||
queryBowl = QueryBowl.of(context);
|
||||
await queryBowl.fetchQuery<T, Outside>(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
mount: widget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
if (oldWidget.externalData != widget.externalData) {
|
||||
QueryBowl.of(context)
|
||||
.fetchQuery(widget.job, externalData: widget.externalData);
|
||||
queryBowl = QueryBowl.of(context);
|
||||
// clearing up the old widget before adding the new updated one
|
||||
// so unmounted zombie widgets don't get piled up
|
||||
queryBowl.getQuery(widget.job.queryKey)?.unmount(oldWidget);
|
||||
queryBowl.fetchQuery(
|
||||
widget.job,
|
||||
externalData: widget.externalData,
|
||||
onData: widget.onData,
|
||||
onError: widget.onError,
|
||||
mount: widget,
|
||||
);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
queryBowl.getQuery(widget.job.queryKey)?.unmount(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final query =
|
||||
QueryBowl.of(context).getQuery<T, Outside>(widget.job.queryKey);
|
||||
queryBowl = QueryBowl.of(context);
|
||||
final query = queryBowl.getQuery<T, Outside>(widget.job.queryKey);
|
||||
if (query == null) return Container();
|
||||
return widget.builder(context, query);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user