added lazy query support

fixed possible race condition of simultanous refetches of the same query
This commit is contained in:
Kingkor Roy Tirtho
2022-06-09 13:33:39 +06:00
parent 9bcdfc9db7
commit 547cf65dd8
4 changed files with 88 additions and 5 deletions
+36
View File
@@ -0,0 +1,36 @@
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart';
final lazyQueryJob = QueryJob<String, String>(
queryKey: "non_enabled_query",
enabled: false,
task: (queryKey, data) {
return Future.delayed(const Duration(milliseconds: 500),
() => "Hello from $queryKey with $data");
});
class LazyQuery extends StatelessWidget {
const LazyQuery({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: QueryBuilder<String, String>(
job: lazyQueryJob,
externalData: "Love",
builder: (context, query) {
return Column(
children: [
Text("Query Data::: ${query.data ?? "Loading"}"),
ElevatedButton(
child: const Text("Fetch Query"),
onPressed: () => query.refetch(),
),
],
);
},
),
);
}
}
+29 -3
View File
@@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:example/another_component.dart'; import 'package:example/another_component.dart';
import 'package:example/lazy_query.dart';
import 'package:example/query_with_external_data.dart'; import 'package:example/query_with_external_data.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -35,7 +36,9 @@ final successJob = QueryJob<String, void>(
final failedJob = QueryJob<String, void>( final failedJob = QueryJob<String, void>(
queryKey: "failure", queryKey: "failure",
task: (queryKey, _) => Future.value("[$queryKey] Failed for unknown reason"), task: (queryKey, _) => Random().nextBool()
? Future.error("[$queryKey] Failed for unknown reason")
: Future.value("Success, you'll get slowly ${Random().nextInt(100)}!"),
); );
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@@ -90,8 +93,20 @@ class _MyHomePageState extends State<MyHomePage> {
job: failedJob, job: failedJob,
externalData: null, externalData: null,
builder: (context, query) { builder: (context, query) {
if (query.hasError) return Text(query.error); return Row(
return Text("Failure. You're a failure ${query.data}"); children: [
if (query.hasError)
Text(
"${query.error}. Retrying: ${query.retryAttempts}"),
if (query.hasData)
Text(
"Success after ${query.retryAttempts}. Data: ${query.data}"),
ElevatedButton(
child: Text("Refetch ${query.queryKey}"),
onPressed: () => query.refetch(),
)
],
);
}, },
), ),
], ],
@@ -106,6 +121,17 @@ class _MyHomePageState extends State<MyHomePage> {
); );
}, },
), ),
const SizedBox(height: 10),
ElevatedButton(
child: const Text("Non Enabled Query Example"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const LazyQuery(),
),
);
},
),
const AnotherComponent(), const AnotherComponent(),
], ],
), ),
@@ -8,6 +8,10 @@ class QueryJob<T extends Object, Outside> {
final Duration? retryDelay; final Duration? retryDelay;
final T? initialData; final T? initialData;
/// If set to false then the initial fetch will not be called & to
/// start the process the user has to call the refetch first
final bool? enabled;
// got from global options // got from global options
final Duration? staleTime; final Duration? staleTime;
@@ -22,5 +26,6 @@ class QueryJob<T extends Object, Outside> {
this.staleTime, this.staleTime,
this.onData, this.onData,
this.onError, this.onError,
this.enabled,
}); });
} }
+18 -2
View File
@@ -20,6 +20,9 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
// all params // all params
final String queryKey; final String queryKey;
QueryTaskFunction<T, Outside> task; QueryTaskFunction<T, Outside> task;
/// The number of times the query should refetch in the time of error
/// before giving up
final int retries; final int retries;
final Duration retryDelay; final Duration retryDelay;
final T? _initialData; final T? _initialData;
@@ -31,9 +34,13 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
T? data; T? data;
dynamic error; dynamic error;
QueryStatus status; QueryStatus status;
/// total count of how many times the query retried to get a successful
/// result
int retryAttempts = 0; int retryAttempts = 0;
DateTime updatedAt; DateTime updatedAt;
int refetchCount = 0; int refetchCount = 0;
bool enabled;
@protected @protected
bool fetched = false; bool fetched = false;
@@ -55,6 +62,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
required this.retries, required this.retries,
required this.retryDelay, required this.retryDelay,
T? initialData, T? initialData,
this.enabled = true,
QueryListener<T>? onData, QueryListener<T>? onData,
QueryListener<dynamic>? onError, QueryListener<dynamic>? onError,
}) : status = QueryStatus.pending, }) : status = QueryStatus.pending,
@@ -69,6 +77,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
Query.fromOptions(QueryJob<T, Outside> options, Query.fromOptions(QueryJob<T, Outside> options,
{required Outside externalData}) {required Outside externalData})
: queryKey = options.queryKey, : queryKey = options.queryKey,
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),
@@ -90,6 +99,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
bool get isRefetching => bool get isRefetching =>
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;
Outside get externalData => _externalData; Outside get externalData => _externalData;
Outside? get prevUsedExternalData => _prevUsedExternalData; Outside? get prevUsedExternalData => _prevUsedExternalData;
@@ -140,6 +150,7 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
Future<T?> fetch() async { Future<T?> fetch() async {
status = QueryStatus.pending; status = QueryStatus.pending;
notifyListeners(); notifyListeners();
if (!enabled) return null;
if (!isStale && hasData) { if (!isStale && hasData) {
return data; return data;
} }
@@ -149,11 +160,16 @@ class Query<T extends Object, Outside> extends ChangeNotifier {
}); });
} }
Future<T?> refetch() { Future<T?> refetch() async {
// cannot let run multiple refetch at the same time. It can cause
// race-condition
if (isRefetching) return null;
status = QueryStatus.refetching; status = QueryStatus.refetching;
refetchCount++; refetchCount++;
// disabling the lazy query bound when query was actually called
if (!enabled) enabled = false;
notifyListeners(); notifyListeners();
return _execute().then((_) => data); return await _execute().then((_) => data);
} }
/// can be used to update the data manually. Can be useful when used /// can be used to update the data manually. Can be useful when used