feat: useInfiniteQuery hook add

This commit is contained in:
Kingkor Roy Tirtho
2023-02-23 23:21:57 +06:00
parent 7fabf44756
commit f9a5207403
7 changed files with 396 additions and 121 deletions
@@ -8,6 +8,7 @@ import 'package:fl_query/src/core/client.dart';
import 'package:fl_query/src/core/infinite_query.dart'; import 'package:fl_query/src/core/infinite_query.dart';
import 'package:fl_query/src/widgets/mixins/rebuilder.dart'; import 'package:fl_query/src/widgets/mixins/rebuilder.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
typedef InfiniteQueryBuilderFn<DataType, ErrorType, PageType> = Widget Function( typedef InfiniteQueryBuilderFn<DataType, ErrorType, PageType> = Widget Function(
BuildContext context, BuildContext context,
@@ -68,15 +69,7 @@ class _InfiniteQueryBuilderState<DataType, ErrorType, PageType>
Future<void> initialize() async { Future<void> initialize() async {
setState(() { setState(() {
query = QueryClient.of(context).createInfiniteQuery( _createQuery();
widget.queryKey,
widget.queryFn,
initialParam: widget.initialPage,
nextPage: widget.nextPage,
retryConfig: widget.retryConfig,
refreshConfig: widget.refreshConfig,
jsonConfig: widget.jsonConfig,
);
if (widget.onData != null) if (widget.onData != null)
dataSubscription = query!.dataStream.listen(widget.onData); dataSubscription = query!.dataStream.listen(widget.onData);
@@ -90,6 +83,18 @@ class _InfiniteQueryBuilderState<DataType, ErrorType, PageType>
} }
} }
void _createQuery() {
query = QueryClient.of(context).createInfiniteQuery(
widget.queryKey,
widget.queryFn,
initialParam: widget.initialPage,
nextPage: widget.nextPage,
retryConfig: widget.retryConfig,
refreshConfig: widget.refreshConfig,
jsonConfig: widget.jsonConfig,
);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -140,8 +145,61 @@ class _InfiniteQueryBuilderState<DataType, ErrorType, PageType>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (query == null) { if (query == null) {
return const SizedBox.shrink(); _createQuery();
} }
return widget.builder(context, query!); return widget.builder(context, query!);
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<
InfiniteQueryBuilderFn<DataType, ErrorType, PageType>>(
'builder',
widget.builder,
));
properties.add(DiagnosticsProperty<InfiniteQueryFn<DataType, PageType>>(
'queryFn',
widget.queryFn,
));
properties
.add(DiagnosticsProperty<InfiniteQueryNextPage<DataType, PageType>>(
'nextPage',
widget.nextPage,
));
properties.add(DiagnosticsProperty<PageType>(
'initialPage',
widget.initialPage,
));
properties.add(DiagnosticsProperty<RetryConfig>(
'retryConfig',
widget.retryConfig,
));
properties.add(DiagnosticsProperty<RefreshConfig>(
'refreshConfig',
widget.refreshConfig,
));
properties.add(DiagnosticsProperty<JsonConfig<DataType>>(
'jsonConfig',
widget.jsonConfig,
));
properties
.add(DiagnosticsProperty<ValueChanged<PageEvent<DataType, PageType>>>(
'onData',
widget.onData,
));
properties
.add(DiagnosticsProperty<ValueChanged<PageEvent<ErrorType, PageType>>>(
'onError',
widget.onError,
));
properties.add(DiagnosticsProperty<bool>(
'enabled',
widget.enabled,
));
properties.add(DiagnosticsProperty<String>(
'queryKey',
widget.queryKey,
));
}
} }
@@ -123,11 +123,7 @@ class _MutationBuilderState<DataType, ErrorType, VariablesType, RecoveryType>
Future<void> initialize() async { Future<void> initialize() async {
setState(() { setState(() {
mutation = QueryClient.of(context).createMutation( _createMutation();
widget.mutationKey,
widget.mutationFn,
retryConfig: widget.retryConfig,
);
subscribeOnMutate(); subscribeOnMutate();
subscribeOnData(); subscribeOnData();
subscribeOnError(); subscribeOnError();
@@ -135,6 +131,14 @@ class _MutationBuilderState<DataType, ErrorType, VariablesType, RecoveryType>
}); });
} }
void _createMutation() {
mutation = QueryClient.of(context).createMutation(
widget.mutationKey,
widget.mutationFn,
retryConfig: widget.retryConfig,
);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -190,7 +194,7 @@ class _MutationBuilderState<DataType, ErrorType, VariablesType, RecoveryType>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (mutation == null) { if (mutation == null) {
return const SizedBox.shrink(); _createMutation();
} }
return widget.builder(context, mutation!); return widget.builder(context, mutation!);
} }
@@ -63,17 +63,20 @@ class _QueryBuilderState<DataType, ErrorType>
StreamSubscription<DataType>? dataSubscription; StreamSubscription<DataType>? dataSubscription;
StreamSubscription<ErrorType>? errorSubscription; StreamSubscription<ErrorType>? errorSubscription;
void _createQuery() {
query = QueryClient.of(context).createQuery(
widget.queryKey,
widget.queryFn,
initial: widget.initial,
retryConfig: widget.retryConfig,
refreshConfig: widget.refreshConfig,
jsonConfig: widget.jsonConfig,
);
}
Future<void> initialize() async { Future<void> initialize() async {
setState(() { setState(() {
query = QueryClient.of(context).createQuery( _createQuery();
widget.queryKey,
widget.queryFn,
initial: widget.initial,
retryConfig: widget.retryConfig,
refreshConfig: widget.refreshConfig,
jsonConfig: widget.jsonConfig,
);
if (widget.onData != null) if (widget.onData != null)
dataSubscription = query!.dataStream.listen(widget.onData); dataSubscription = query!.dataStream.listen(widget.onData);
if (widget.onData != null) if (widget.onData != null)
@@ -135,7 +138,7 @@ class _QueryBuilderState<DataType, ErrorType>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (query == null) { if (query == null) {
return const SizedBox.shrink(); _createQuery();
} }
return widget.builder(context, query!); return widget.builder(context, query!);
} }
@@ -1,40 +1,61 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:fl_query_hooks_example/models/product.dart'; import 'package:fl_query_hooks_example/models/product.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';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
class InfiniteQueryPageWidget extends StatefulWidget { class InfiniteQueryPageWidget extends HookWidget {
const InfiniteQueryPageWidget({super.key}); const InfiniteQueryPageWidget({super.key});
@override
State<InfiniteQueryPageWidget> createState() =>
_InfiniteQueryPageWidgetState();
}
class _InfiniteQueryPageWidgetState extends State<InfiniteQueryPageWidget> {
final controller = ScrollController();
@override
void initState() {
super.initState();
controller.addListener(() async {
if (controller.position.pixels == controller.position.maxScrollExtent) {
final query = QueryClient.of(context).getInfiniteQuery("products");
await query?.fetchNext();
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = useScrollController();
final query = useInfiniteQuery<PagedProducts, ClientException, int>(
"products",
(page) async {
final res = await get(Uri.parse(
"https://dummyjson.com/products?limit=10&skip=${page * 10}",
));
if (res.statusCode == 200) {
return PagedProducts.fromJson(jsonDecode(res.body));
} else {
throw ClientException(res.statusCode.toString(), res.request?.url);
}
},
nextPage: (lastPage, pages) {
if (pages.isNotEmpty &&
pages.last.products.length < pages[lastPage].limit) {
/// returning [null] will set [hasNextPage] to [false]
return null;
}
return lastPage + 1;
},
initialPage: 0,
jsonConfig: JsonConfig(
fromJson: (json) => PagedProducts.fromJson(json),
toJson: (data) => data.toJson(),
),
);
useEffect(() {
listener() async {
if (controller.position.pixels == controller.position.maxScrollExtent) {
final query = QueryClient.of(context).getInfiniteQuery("products");
await query?.fetchNext();
}
}
controller.addListener(listener);
return () => controller.removeListener(listener);
}, [controller]);
final products = query.pages.map((e) => e.products).expand((e) => e);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Infinite Query'), title: const Text('Infinite Query'),
@@ -48,58 +69,27 @@ class _InfiniteQueryPageWidgetState extends State<InfiniteQueryPageWidget> {
child: Text(query?.pages.length.toString() ?? "-69"), child: Text(query?.pages.length.toString() ?? "-69"),
); );
}), }),
body: InfiniteQueryBuilder<PagedProducts, ClientException, int>( body: !query.hasPages
"products", ? const Center(
(page) async {
final res = await get(Uri.parse(
"https://dummyjson.com/products?limit=10&skip=${page * 10}",
));
if (res.statusCode == 200) {
return PagedProducts.fromJson(jsonDecode(res.body));
} else {
throw ClientException(res.statusCode.toString(), res.request?.url);
}
},
nextPage: (lastPage, pages) {
if (pages.isNotEmpty &&
pages.last.products.length < pages[lastPage].limit) {
/// returning [null] will set [hasNextPage] to [false]
return null;
}
return lastPage + 1;
},
initialPage: 0,
jsonConfig: JsonConfig(
fromJson: (json) => PagedProducts.fromJson(json),
toJson: (data) => data.toJson(),
),
builder: (context, query) {
final products = query.pages.map((e) => e.products).expand((e) => e);
if (!query.hasPages) {
return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); )
} : ListView(
return ListView( controller: controller,
controller: controller, children: [
children: [ for (final product in products)
for (final product in products) ListTile(
ListTile( title: Text(product.title),
title: Text(product.title), subtitle: Text(product.description),
subtitle: Text(product.description), leading: Image.network(product.thumbnail),
leading: Image.network(product.thumbnail), ),
), if (query.hasNextPage)
if (query.hasNextPage) const Center(
const Center( child: CircularProgressIndicator(),
child: CircularProgressIndicator(), ),
), if (query.hasErrors)
if (query.hasErrors) ...query.errors.map((e) => Text(e.message)).toList(),
...query.errors.map((e) => Text(e.message)).toList(), ],
], ),
);
},
),
); );
} }
} }
@@ -1,4 +1,5 @@
library fl_query_hooks; library fl_query_hooks;
export 'src/use_query.dart';
export 'src/use_query_client.dart'; export 'src/use_query_client.dart';
export 'src/use_query.dart';
export 'src/use_infinite_query.dart';
@@ -0,0 +1,223 @@
import 'dart:async';
import 'package:fl_query/src/collections/default_configs.dart';
import 'package:fl_query/src/collections/json_config.dart';
import 'package:fl_query/src/collections/refresh_config.dart';
import 'package:fl_query/src/collections/retry_config.dart';
import 'package:fl_query/src/core/client.dart';
import 'package:fl_query/src/core/infinite_query.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
InfiniteQuery<DataType, ErrorType, PageType>
useInfiniteQuery<DataType, ErrorType, PageType>(
String queryKey,
InfiniteQueryFn<DataType, PageType> queryFn, {
required InfiniteQueryNextPage<DataType, PageType> nextPage,
required PageType initialPage,
RetryConfig retryConfig = DefaultConstants.retryConfig,
RefreshConfig refreshConfig = DefaultConstants.refreshConfig,
JsonConfig<DataType>? jsonConfig,
ValueChanged<PageEvent<DataType, PageType>>? onData,
ValueChanged<PageEvent<ErrorType, PageType>>? onError,
bool enabled = true,
}) {
return use(UseInfiniteQuery(
queryKey,
queryFn,
nextPage: nextPage,
initialPage: initialPage,
retryConfig: retryConfig,
refreshConfig: refreshConfig,
jsonConfig: jsonConfig,
onData: onData,
onError: onError,
enabled: enabled,
));
}
class UseInfiniteQuery<DataType, ErrorType, PageType>
extends Hook<InfiniteQuery<DataType, ErrorType, PageType>> {
final InfiniteQueryFn<DataType, PageType> queryFn;
final String queryKey;
final PageType initialPage;
final InfiniteQueryNextPage<DataType, PageType> nextPage;
final RetryConfig retryConfig;
final RefreshConfig refreshConfig;
final JsonConfig<DataType>? jsonConfig;
final ValueChanged<PageEvent<DataType, PageType>>? onData;
final ValueChanged<PageEvent<ErrorType, PageType>>? onError;
// widget specific
final bool enabled;
const UseInfiniteQuery(
this.queryKey,
this.queryFn, {
required this.nextPage,
required this.initialPage,
this.retryConfig = DefaultConstants.retryConfig,
this.refreshConfig = DefaultConstants.refreshConfig,
this.jsonConfig,
this.onData,
this.onError,
this.enabled = true,
super.keys,
}) : assert(
(jsonConfig != null && enabled) || jsonConfig == null,
'jsonConfig is only supported when enabled is true',
);
@override
createState() => _UseInfiniteQueryState<DataType, ErrorType, PageType>();
}
class _UseInfiniteQueryState<DataType, ErrorType, PageType> extends HookState<
InfiniteQuery<DataType, ErrorType, PageType>,
UseInfiniteQuery<DataType, ErrorType, PageType>> {
InfiniteQuery<DataType, ErrorType, PageType>? query;
VoidCallback? removeListener;
StreamSubscription<PageEvent<DataType, PageType>>? dataSubscription;
StreamSubscription<PageEvent<ErrorType, PageType>>? errorSubscription;
void rebuild([_]) {
setState(() {});
}
Future<void> initialize() async {
setState(() {
_createQuery();
if (hook.onData != null)
dataSubscription = query!.dataStream.listen(hook.onData);
if (hook.onError != null)
errorSubscription = query!.errorStream.listen(hook.onError);
removeListener = query!.addListener(rebuild);
});
if (hook.enabled) {
await query!.fetch();
}
}
void _createQuery() {
query = QueryClient.of(context).createInfiniteQuery(
hook.queryKey,
hook.queryFn,
initialParam: hook.initialPage,
nextPage: hook.nextPage,
retryConfig: hook.retryConfig,
refreshConfig: hook.refreshConfig,
jsonConfig: hook.jsonConfig,
);
}
@override
void initHook() {
super.initHook();
WidgetsBinding.instance.addPostFrameCallback((_) {
initialize();
});
}
@override
void dispose() {
dataSubscription?.cancel();
errorSubscription?.cancel();
removeListener?.call();
super.dispose();
}
@override
void didUpdateHook(oldHook) {
super.didUpdateHook(oldHook);
if (oldHook.queryKey != hook.queryKey) {
dataSubscription?.cancel();
errorSubscription?.cancel();
removeListener?.call();
initialize();
return;
} else if (oldHook.enabled != hook.enabled && hook.enabled) {
query!.fetch();
}
if (oldHook.queryFn != hook.queryFn) {
query!.updateQueryFn(hook.queryFn);
}
if (oldHook.nextPage != hook.nextPage) {
query!.updateNextPageFn(hook.nextPage);
}
if (oldHook.onData != hook.onData) {
dataSubscription?.cancel();
if (hook.onData != null)
dataSubscription = query!.dataStream.listen(hook.onData);
}
if (oldHook.onError != hook.onError) {
errorSubscription?.cancel();
if (hook.onError != null)
errorSubscription = query!.errorStream.listen(hook.onError);
}
}
@override
build(BuildContext context) {
if (query == null) {
_createQuery();
}
return query!;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<InfiniteQueryFn<DataType, PageType>>(
'queryFn',
hook.queryFn,
));
properties
.add(DiagnosticsProperty<InfiniteQueryNextPage<DataType, PageType>>(
'nextPage',
hook.nextPage,
));
properties.add(DiagnosticsProperty<PageType>(
'initialPage',
hook.initialPage,
));
properties.add(DiagnosticsProperty<RetryConfig>(
'retryConfig',
hook.retryConfig,
));
properties.add(DiagnosticsProperty<RefreshConfig>(
'refreshConfig',
hook.refreshConfig,
));
properties.add(DiagnosticsProperty<JsonConfig<DataType>>(
'jsonConfig',
hook.jsonConfig,
));
properties
.add(DiagnosticsProperty<ValueChanged<PageEvent<DataType, PageType>>>(
'onData',
hook.onData,
));
properties
.add(DiagnosticsProperty<ValueChanged<PageEvent<ErrorType, PageType>>>(
'onError',
hook.onError,
));
properties.add(DiagnosticsProperty<bool>(
'enabled',
hook.enabled,
));
properties.add(DiagnosticsProperty<String>(
'queryKey',
hook.queryKey,
));
}
}
+13 -17
View File
@@ -83,17 +83,20 @@ class _UseQueryState<DataType, ErrorType> extends HookState<
setState(() {}); setState(() {});
} }
void _createQuery() {
query = QueryClient.of(context).createQuery(
hook.queryKey,
hook.queryFn,
initial: hook.initial,
retryConfig: hook.retryConfig,
refreshConfig: hook.refreshConfig,
jsonConfig: hook.jsonConfig,
);
}
Future<void> initialize() async { Future<void> initialize() async {
setState(() { setState(() {
query = QueryClient.of(context).createQuery( _createQuery();
hook.queryKey,
hook.queryFn,
initial: hook.initial,
retryConfig: hook.retryConfig,
refreshConfig: hook.refreshConfig,
jsonConfig: hook.jsonConfig,
);
if (hook.onData != null) if (hook.onData != null)
dataSubscription = query!.dataStream.listen(hook.onData); dataSubscription = query!.dataStream.listen(hook.onData);
if (hook.onData != null) if (hook.onData != null)
@@ -155,14 +158,7 @@ class _UseQueryState<DataType, ErrorType> extends HookState<
@override @override
Query<DataType, ErrorType> build(BuildContext context) { Query<DataType, ErrorType> build(BuildContext context) {
if (query == null) { if (query == null) {
query = QueryClient.of(context).createQuery( _createQuery();
hook.queryKey,
hook.queryFn,
initial: hook.initial,
retryConfig: hook.retryConfig,
refreshConfig: hook.refreshConfig,
jsonConfig: hook.jsonConfig,
);
} }
return query!; return query!;
} }