cache time & garbage collection support added

onData & onError callbacks WIP
This commit is contained in:
Kingkor Roy Tirtho
2022-06-12 15:18:43 +06:00
parent 547cf65dd8
commit f0a7579ea2
7 changed files with 130 additions and 37 deletions
+5 -1
View File
@@ -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}",
);
}
}
+9 -5
View File
@@ -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");
+2 -4
View File
@@ -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,
});
}
+43 -6
View File
@@ -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;
}
}
+32 -15
View File
@@ -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();
}
+38 -6
View File
@@ -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);
}