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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final lol = QueryBowl.of(context).getQuery<String, void>("greetings"); 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(); 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>( final successJob = QueryJob<String, void>(
queryKey: "greetings", queryKey: "greetings",
task: (queryKey, _) => Future.delayed(const Duration(seconds: 2), task: (queryKey, _) => Future.delayed(const Duration(seconds: 2),
() => "Welcome ($queryKey) ${Random.secure().nextInt(100)}")); () => "Welcome ($queryKey) ${Random.secure().nextInt(100)}"),
);
final failedJob = QueryJob<String, void>( final failedJob = QueryJob<String, void>(
queryKey: "failure", queryKey: "failure",
task: (queryKey, _) => Random().nextBool() task: (queryKey, _) => Random().nextBool()
? Future.error("[$queryKey] Failed for unknown reason") ? 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 { class MyHomePage extends StatefulWidget {
@@ -97,7 +100,8 @@ class _MyHomePageState extends State<MyHomePage> {
children: [ children: [
if (query.hasError) if (query.hasError)
Text( Text(
"${query.error}. Retrying: ${query.retryAttempts}"), "${query.error}. Retrying: ${query.retryAttempts}",
),
if (query.hasData) if (query.hasData)
Text( Text(
"Success after ${query.retryAttempts}. Data: ${query.data}"), "Success after ${query.retryAttempts}. Data: ${query.data}"),
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
final jobWithExternalData = QueryJob<String, String>( final jobWithExternalData = QueryJob<String, String>(
queryKey: "external_data", queryKey: "external_data",
cacheTime: const Duration(seconds: 10),
task: (queryKey, data) { task: (queryKey, data) {
return Future.delayed(const Duration(milliseconds: 500), return Future.delayed(const Duration(milliseconds: 500),
() => "Hello from $queryKey with $data"); () => "Hello from $queryKey with $data");
+2 -4
View File
@@ -14,9 +14,8 @@ class QueryJob<T extends Object, Outside> {
// got from global options // got from global options
final Duration? staleTime; final Duration? staleTime;
final Duration? cacheTime;
final QueryListener<T>? onData;
final QueryListener<dynamic>? onError;
QueryJob({ QueryJob({
required this.queryKey, required this.queryKey,
required this.task, required this.task,
@@ -24,8 +23,7 @@ class QueryJob<T extends Object, Outside> {
this.retryDelay, this.retryDelay,
this.initialData, this.initialData,
this.staleTime, this.staleTime,
this.onData, this.cacheTime,
this.onError,
this.enabled, this.enabled,
}); });
} }
+43 -6
View File
@@ -29,6 +29,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
// got from global options // got from global options
final Duration _staleTime; final Duration _staleTime;
final Duration _cacheTime;
// all properties // all properties
T? data; T? data;
@@ -54,10 +55,16 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
Outside? _prevUsedExternalData; 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({ Query({
required this.queryKey, required this.queryKey,
required this.task, required this.task,
required Duration staleTime, required Duration staleTime,
required Duration cacheTime,
required Outside externalData, required Outside externalData,
required this.retries, required this.retries,
required this.retryDelay, required this.retryDelay,
@@ -67,6 +74,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
QueryListener<dynamic>? onError, QueryListener<dynamic>? onError,
}) : status = QueryStatus.pending, }) : status = QueryStatus.pending,
_staleTime = staleTime, _staleTime = staleTime,
_cacheTime = cacheTime,
_initialData = initialData, _initialData = initialData,
_externalData = externalData, _externalData = externalData,
data = initialData, data = initialData,
@@ -74,18 +82,22 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
_onError = onError, _onError = onError,
updatedAt = DateTime.now(); updatedAt = DateTime.now();
Query.fromOptions(QueryJob<T, Outside> options, Query.fromOptions(
{required Outside externalData}) QueryJob<T, Outside> options, {
: queryKey = options.queryKey, required Outside externalData,
QueryListener<T>? onData,
QueryListener<dynamic>? onError,
}) : queryKey = options.queryKey,
enabled = options.enabled ?? true, enabled = options.enabled ?? true,
task = options.task, task = options.task,
retries = options.retries ?? 3, retries = options.retries ?? 3,
retryDelay = options.retryDelay ?? const Duration(milliseconds: 200), 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, _initialData = options.initialData,
_externalData = externalData, _externalData = externalData,
_onData = options.onData, _onData = onData,
_onError = options.onError, _onError = onError,
data = options.initialData, data = options.initialData,
status = QueryStatus.pending, status = QueryStatus.pending,
updatedAt = DateTime.now(); updatedAt = DateTime.now();
@@ -100,11 +112,29 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
status == QueryStatus.refetching && (data != null || error != null); status == QueryStatus.refetching && (data != null || error != null);
bool get isSucceeded => status == QueryStatus.succeed && data != null; bool get isSucceeded => status == QueryStatus.succeed && data != null;
bool get isIdle => isSucceeded && error == null; bool get isIdle => isSucceeded && error == null;
bool get isInactive => _mounts.isEmpty;
Outside get externalData => _externalData; Outside get externalData => _externalData;
Outside? get prevUsedExternalData => _prevUsedExternalData; Outside? get prevUsedExternalData => _prevUsedExternalData;
// all methods // 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 /// Calls the task function & doesn't check if there's already
/// cached data available /// cached data available
Future<void> _execute() async { 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; 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 { class QueryBowlScope extends StatefulWidget {
final Widget child; final Widget child;
final Duration staleTime; final Duration staleTime;
final Duration cacheTime;
/// used for periodically checking if any query got stale. /// used for periodically checking if any query got stale.
/// If none is supplied then half of the value of staleTime is used /// If none is supplied then half of the value of staleTime is used
final Duration? refreshInterval; final Duration refreshInterval;
const QueryBowlScope({ const QueryBowlScope({
required this.child, required this.child,
this.staleTime = const Duration(minutes: 5), this.staleTime = const Duration(milliseconds: 500),
this.refreshInterval, this.cacheTime = const Duration(minutes: 5),
this.refreshInterval = const Duration(minutes: 5),
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -33,10 +35,7 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
super.initState(); super.initState();
queries = {}; queries = {};
refreshIntervalTimer = Timer.periodic( refreshIntervalTimer = Timer.periodic(
widget.refreshInterval ?? widget.refreshInterval,
Duration(
milliseconds: (widget.staleTime.inMilliseconds / 2).round(),
),
_checkAndUpdateStaleQueriesOnBg, _checkAndUpdateStaleQueriesOnBg,
); );
} }
@@ -58,19 +57,25 @@ class _QueryBowlScopeState extends State<QueryBowlScope> {
void _listenToQueryUpdate() { void _listenToQueryUpdate() {
for (final query in queries) { for (final query in queries) {
query.addListener(updateQueries); query.addListener(() => updateQueries(query));
} }
} }
void _disposeListeners() { void _disposeListeners() {
for (final query in queries) { for (final query in queries) {
query.removeListener(updateQueries); query.removeListener(() => updateQueries(query));
} }
} }
void updateQueries() { void updateQueries(Query query) {
setState(() { 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, _queries = queries,
super(child: child, key: key); super(child: child, key: key);
Future<T?> fetchQuery<T extends Object, Outside>(QueryJob<T, Outside> options, Future<T?> fetchQuery<T extends Object, Outside>(
{required Outside externalData}) async { QueryJob<T, Outside> options, {
required Outside externalData,
final QueryListener<T>? onData,
final QueryListener<dynamic>? onError,
Widget? mount,
}) async {
final prevQuery = final prevQuery =
_queries.firstWhereOrNull((q) => q.queryKey == options.queryKey); _queries.firstWhereOrNull((q) => q.queryKey == options.queryKey);
if (prevQuery is Query<T, Outside>) { if (prevQuery is Query<T, Outside>) {
@@ -123,16 +133,23 @@ class QueryBowl extends InheritedWidget {
// changed // changed
final hasExternalDataChanged = final hasExternalDataChanged =
prevQuery.prevUsedExternalData != externalData; prevQuery.prevUsedExternalData != externalData;
if (mount != null) prevQuery.mount(mount);
if (!prevQuery.hasData || hasExternalDataChanged) { if (!prevQuery.hasData || hasExternalDataChanged) {
if (hasExternalDataChanged) prevQuery.setExternalData(externalData); if (hasExternalDataChanged) prevQuery.setExternalData(externalData);
return prevQuery.fetched return prevQuery.fetched
? await prevQuery.refetch() ? await prevQuery.refetch()
: await prevQuery.fetch(); : await prevQuery.fetch();
} }
// mounting the widget that is using the query in the prevQuery
return prevQuery.data; return prevQuery.data;
} }
final query = final query = Query<T, Outside>.fromOptions(
Query<T, Outside>.fromOptions(options, externalData: externalData); options,
externalData: externalData,
onData: onData,
onError: onError,
);
if (mount != null) query.mount(mount);
_addQuery<T, Outside>(query); _addQuery<T, Outside>(query);
return await query.fetch(); 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 QueryJob<T, Outside> job;
final Outside externalData; 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({ const QueryBuilder({
required this.job, required this.job,
required this.externalData, required this.externalData,
required this.builder, required this.builder,
this.onData,
this.onError,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -22,28 +31,51 @@ class QueryBuilder<T extends Object, Outside> extends StatefulWidget {
class _QueryBuilderState<T extends Object, Outside> class _QueryBuilderState<T extends Object, Outside>
extends State<QueryBuilder<T, Outside>> { extends State<QueryBuilder<T, Outside>> {
late QueryBowl queryBowl;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await QueryBowl.of(context).fetchQuery<T, Outside>(widget.job, queryBowl = QueryBowl.of(context);
externalData: widget.externalData); await queryBowl.fetchQuery<T, Outside>(
widget.job,
externalData: widget.externalData,
onData: widget.onData,
onError: widget.onError,
mount: widget,
);
}); });
} }
@override @override
void didUpdateWidget(covariant oldWidget) { void didUpdateWidget(covariant oldWidget) {
if (oldWidget.externalData != widget.externalData) { if (oldWidget.externalData != widget.externalData) {
QueryBowl.of(context) queryBowl = QueryBowl.of(context);
.fetchQuery(widget.job, externalData: widget.externalData); // 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); super.didUpdateWidget(oldWidget);
} }
@override
void dispose() {
queryBowl.getQuery(widget.job.queryKey)?.unmount(widget);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final query = queryBowl = QueryBowl.of(context);
QueryBowl.of(context).getQuery<T, Outside>(widget.job.queryKey); final query = queryBowl.getQuery<T, Outside>(widget.job.queryKey);
if (query == null) return Container(); if (query == null) return Container();
return widget.builder(context, query); return widget.builder(context, query);
} }