cache time & garbage collection support added
onData & onError callbacks WIP
This commit is contained in:
@@ -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}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user