docs: add lazy, dynamic, persisting query

This commit is contained in:
Kingkor Roy Tirtho
2023-10-18 10:53:34 +06:00
parent b913de171d
commit 99a3de4c03
5 changed files with 133 additions and 248 deletions
-27
View File
@@ -1,27 +0,0 @@
---
title: Dynamic Mutations
sidebar_position: 9
---
Just like [Dynamic Queries](/docs/basics/DynamicQueries), `MutationJob.withVariableKey` makes the mutation dynamic. Both of them are completely same
```dart
final mutationVariableKeyJob = MutationJob.withVariableKey<String, double>(
preMutationKey: "mutation-example",
task: (mutationKey, variables) {
return MyAPI.submit({...variables, id: getVariable(mutationKey)});
},
);
```
In the case of Mutation, we've `preMutationKey` instead of `preQueryKey`
You can use the dynamic Mutation Job just like any other `MutationJob` except you've to invoke the defined dynamic mutation & pass the `variable-mutation-key` as the first argument.
```dart
MutationBuilder<String, double>(
job: mutationVariableKeyJob(id),
builder: (context, mutation) {...},
)
```
-66
View File
@@ -1,66 +0,0 @@
---
title: Dynamic Queries
sidebar_position: 8
---
All this time we've using queries by defining a Query Key in the `QueryJob`. But what if your widget needs to fetch data from of a dynamic id that is only known at runtime? This is where `QueryJob.withVariableKey` comes to play. It allows you to define query-key at runtime. It makes a query dynamic
```dart
final queryVariableKeyJob =
QueryJob.withVariableKey<String, void>(
prevQueryKey: "variable-query",
task: (queryKey, externalData) {
return Future.delayed(
const Duration(milliseconds: 500),
() => "QueryKey:${getVariable(queryKey)}",
);
},
);
```
Optionally, we can provide a `prevQueryKey` to make the QueryJob distinguishable from other dynamic Queries or just to keep them in a group. If you use `prevQueryKey`, you can use `getVariable` to extract the value of the variable from the `queryKey`.
:::warning
Don't use `externalData` to make a query dynamic. Using `externalData` & `refetchOnExternalDataChange: true`, you may be able to achieve similar result but it'll replace the previously fetched data with the new data instead of creating a separate `Query` instance for the new variable-query-key & it's data
:::
Now, let's use the Job with a QueryBuilder inside an actual widget:
```dart
class ExampleState extends State<Example>{
late double id;
@override
void initState() {
super.initState();
id = Random().nextDouble() * 200;
}
@override
Widget build(BuildContext context) {
return QueryBuilder<String, void>(
job: queryVariableKeyJob(id.toString()),
externalData: null,
builder: (context, query) {
if (!query.hasData) {
return const CircularProgressIndicator();
}
return Row(
children: [
Text("Query Result: ${query.data}"),
ElevatedButton(
child: const Text("New Id"),
onPressed: () {
setState(() {
id = Random().nextDouble() * 200;
});
},
),
],
);
},
);
}
}
```
Here, everything is same except to pass the `variable-query-key` to the dynamic `Query` you'll have to invoke the defined job (in this case it's `queryVariableKeyJob`) & pass the `variable-query-key` as an argument.
-55
View File
@@ -1,55 +0,0 @@
---
title: Lazy Query
sidebar_position: 7
---
If you ever want to disable a query from automatically running, you can use the enabled = false option in [`QueryJob`](/docs/basics/QueryJob)
When `enabled` is false:
- If the query has initial data
- The query will be initialized in the status === 'success' or isSuccess state.
- If the query does not have any data
- The query will start in the status === 'idle' or isIdle state.
- The query will not automatically `fetch` on mount.
- The query will not automatically `refetch` in the background when new instances mount or new instances appearing
- The query will ignore query client `invalidateQueries` and `refetchQueries` calls that would normally result in the query refetching.
- `refetch` can be used to manually trigger the query to fetch
Here's a basic QueryJob that won't run automatically:
```dart
final lazyQueryJob = QueryJob<String, String>(
queryKey: "lazy-query",
enabled: false,
task: (queryKey, data) {
return Future.delayed(const Duration(milliseconds: 500),
() => "Result: key=$queryKey value=$data");
},
);
```
Let's use this Lazy Query Job in our example:
```dart
@override
Widget build(BuildContext context) {
return QueryBuilder<String, String>(
// This query won't run automatically anyway unless the [refetch] method
// is called
job: lazyQueryJob,
externalData: "I can get your heart beat beat beat beating like",
builder: (context, query) {
return Row(
children: [
Text("Current Data: ${query.data ?? "Loading"}"),
ElevatedButton(
child: const Text("Refetch Query"),
onPressed: () => query.refetch(),
),
],
);
},
);
}
```
-99
View File
@@ -1,99 +0,0 @@
---
title: Paginated/Lagged Query
sidebar_position: 10
---
Rendering paginated data is a very common UI pattern and in Fl-Query, it "just works" by including the page information in the query key:
```dart
final queryVariableKeyJob = QueryJob.withVariableKey<String, void>(
task: (queryKey, externalData) {
return MyAPI.getData(id: getVariable(queryKey));
},
);
/// inside a widget build method
QueryBuilder(
job: queryVariableKeyJob(id),
externalData: null,
builder: (context, query){...}
)
```
However, if you run this simple example, you might notice something strange:
**The UI jumps in and out of the `success` and `loading` states because each new page is treated like a brand new query.**
This experience is not optimal and unfortunately is how many tools today insist on working. But not Fl-Query! As you may have guessed, Fl-Query comes with an awesome feature called `keepPreviousData` that allows us to get around this.
## Better Paginated Queries with `keepPreviousData`
Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use just `QueryJob.withVariableKey`, **it would still technically work fine**, but the UI would jump in and out of the `success` and `loading` states as different queries are created and destroyed for each page or cursor. By setting `keepPreviousData` to `true` we get a few new things:
- **The data from the last successful fetch available while new data is being requested, even though the query key has changed**.
- When the new data arrives, the previous `data` is seamlessly swapped to show the new data.
- `isPreviousData` is made available to know what data the query is currently providing you
```dart
final todoJob = QueryJob.withVariableKey<Map, void>(
preQueryKey: "todo",
task: (queryKey, _) async {
final res = await http.get(
Uri.parse(
"https://jsonplaceholder.typicode.com/todos/${getVariable(queryKey)}"),
);
return jsonDecode(res.body);
},
keepPreviousData: true,
);
class QueryPreviousDataExample extends StatefulWidget {
const QueryPreviousDataExample({Key? key}) : super(key: key);
@override
State<QueryPreviousDataExample> createState() =>
_QueryPreviousDataExampleState();
}
class _QueryPreviousDataExampleState extends State<QueryPreviousDataExample> {
int id = 1;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QueryBuilder(
job: todoJob(id.toString()),
externalData: null,
builder: (context, query) {
if (query.hasError) return Text(query.error.toString());
if (!query.hasData) return const CircularProgressIndicator();
return Text(jsonEncode(query.data ?? {}));
}),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
setState(() {
id -= 1;
});
},
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
id += 1;
});
},
),
],
)
],
);
}
}
```
+133 -1
View File
@@ -124,4 +124,136 @@ await query.setData("new data"); // type of 'data' has to match the DataType
:::tip
You can learn more about Optimistic Updates in the [Mutation Tutorial](/docs/basics/mutations)
:::
:::
### Dynamic key
You can also use dynamic keys with `QueryBuilder` and `useQuery`. With dart's String interpolation, you can pass dynamic keys to the query
<Tabs>
<TabItem value="vanilla" label="Vanilla">
```dart
QueryBuilder<String, HttpException>(
"todos/$todoId",
() => api.getTodo(todoId),
builder: (context, query) {
/* ... */
},
);
```
</TabItem>
<TabItem value="flutter_hooks" label="Flutter Hooks">
```dart
useQuery<String, HttpException>(
"todos/$todoId",
() => api.getTodo(todoId),
);
```
</TabItem>
</Tabs>
> For every new `todoId`, a new query will be created and cached separately
### Lazy Query
By default queries are executed immediately after they are mounted. But you can also make them lazy by passing `enabled: false` to the `QueryBuilder` or `useQuery`
<Tabs>
<TabItem value="vanilla" label="Vanilla">
```dart
QueryBuilder<String, HttpException>(
"lazy-todos",
() => api.getTodos(),
enabled: false,
builder: (context, query) {
/* ... */
},
);
```
</TabItem>
<TabItem value="flutter_hooks" label="Flutter Hooks">
```dart
useQuery<String, HttpException>(
"lazy-todos",
() => api.getTodos(),
enabled: false,
);
```
</TabItem>
</Tabs>
Now these queries won't be executed as soon as they're mounted. Until `Query.refresh()` or `Query.fetch()` is called these will stay in initial state. If `initial` data was passed, it'll be used until the query is refreshed. Same goes for persisting queries
### Persisting Queries
Queries can be persisted by passing `jsonConfig` argument to the `QueryBuilder` or `useQuery`. Persisted queries are stored in [hive](https://docs.hivedb.dev/) cache and are available even after the app is restarted
First make sure your custom data type is json serializable. You can use [json_serializable](https://pub.dev/packages/json_serializable) package to generate `toJson` and `fromJson` methods for your data type
```dart
import 'package:json_annotation/json_annotation.dart';
part 'todo.g.dart';
@JsonSerializable()
class Todo{
final String id;
final String title;
final bool completed;
Todo({
required this.id,
required this.title,
required this.completed,
});
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
Map<String, dynamic> toJson() => _$TodoToJson(this);
}
```
<Tabs>
<TabItem value="vanilla" label="Vanilla">
```dart
QueryBuilder<Todo, HttpException>(
"todos",
() => api.getTodos(),
jsonConfig: JsonConfig(
fromJson: (json) => Todo.fromJson(json),
toJson: (todo) => todo.toJson(),
),
builder: (context, query) {
/* ... */
},
);
```
</TabItem>
<TabItem value="flutter_hooks" label="Flutter Hooks">
```dart
useQuery<Todo, HttpException>(
"todos",
() => api.getTodos(),
jsonConfig: JsonConfig(
fromJson: (json) => Todo.fromJson(json),
toJson: (todo) => todo.toJson(),
),
);
```
</TabItem>
</Tabs>
Right now due to lack of reflection support and compile time macros we're unable to serialize any data type on the fly.
That's why `JsonConfig` is required. Otherwise, a simple `persistToDisk: true` would have been enough