diff --git a/packages/fl_query/lib/src/widgets/infinite_query_builder.dart b/packages/fl_query/lib/src/widgets/infinite_query_builder.dart index 792a3a3..e6c657a 100644 --- a/packages/fl_query/lib/src/widgets/infinite_query_builder.dart +++ b/packages/fl_query/lib/src/widgets/infinite_query_builder.dart @@ -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/widgets/mixins/rebuilder.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/diagnostics.dart'; typedef InfiniteQueryBuilderFn = Widget Function( BuildContext context, @@ -68,15 +69,7 @@ class _InfiniteQueryBuilderState Future initialize() async { setState(() { - query = QueryClient.of(context).createInfiniteQuery( - widget.queryKey, - widget.queryFn, - initialParam: widget.initialPage, - nextPage: widget.nextPage, - retryConfig: widget.retryConfig, - refreshConfig: widget.refreshConfig, - jsonConfig: widget.jsonConfig, - ); + _createQuery(); if (widget.onData != null) dataSubscription = query!.dataStream.listen(widget.onData); @@ -90,6 +83,18 @@ class _InfiniteQueryBuilderState } } + 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 void initState() { super.initState(); @@ -140,8 +145,61 @@ class _InfiniteQueryBuilderState @override Widget build(BuildContext context) { if (query == null) { - return const SizedBox.shrink(); + _createQuery(); } return widget.builder(context, query!); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty< + InfiniteQueryBuilderFn>( + 'builder', + widget.builder, + )); + properties.add(DiagnosticsProperty>( + 'queryFn', + widget.queryFn, + )); + properties + .add(DiagnosticsProperty>( + 'nextPage', + widget.nextPage, + )); + properties.add(DiagnosticsProperty( + 'initialPage', + widget.initialPage, + )); + properties.add(DiagnosticsProperty( + 'retryConfig', + widget.retryConfig, + )); + properties.add(DiagnosticsProperty( + 'refreshConfig', + widget.refreshConfig, + )); + properties.add(DiagnosticsProperty>( + 'jsonConfig', + widget.jsonConfig, + )); + properties + .add(DiagnosticsProperty>>( + 'onData', + widget.onData, + )); + properties + .add(DiagnosticsProperty>>( + 'onError', + widget.onError, + )); + properties.add(DiagnosticsProperty( + 'enabled', + widget.enabled, + )); + properties.add(DiagnosticsProperty( + 'queryKey', + widget.queryKey, + )); + } } diff --git a/packages/fl_query/lib/src/widgets/mutation_builder.dart b/packages/fl_query/lib/src/widgets/mutation_builder.dart index 6d882f0..888a77e 100644 --- a/packages/fl_query/lib/src/widgets/mutation_builder.dart +++ b/packages/fl_query/lib/src/widgets/mutation_builder.dart @@ -123,11 +123,7 @@ class _MutationBuilderState Future initialize() async { setState(() { - mutation = QueryClient.of(context).createMutation( - widget.mutationKey, - widget.mutationFn, - retryConfig: widget.retryConfig, - ); + _createMutation(); subscribeOnMutate(); subscribeOnData(); subscribeOnError(); @@ -135,6 +131,14 @@ class _MutationBuilderState }); } + void _createMutation() { + mutation = QueryClient.of(context).createMutation( + widget.mutationKey, + widget.mutationFn, + retryConfig: widget.retryConfig, + ); + } + @override void initState() { super.initState(); @@ -190,7 +194,7 @@ class _MutationBuilderState @override Widget build(BuildContext context) { if (mutation == null) { - return const SizedBox.shrink(); + _createMutation(); } return widget.builder(context, mutation!); } diff --git a/packages/fl_query/lib/src/widgets/query_builder.dart b/packages/fl_query/lib/src/widgets/query_builder.dart index 4432b77..53000ae 100644 --- a/packages/fl_query/lib/src/widgets/query_builder.dart +++ b/packages/fl_query/lib/src/widgets/query_builder.dart @@ -63,17 +63,20 @@ class _QueryBuilderState StreamSubscription? dataSubscription; StreamSubscription? 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 initialize() async { setState(() { - query = QueryClient.of(context).createQuery( - widget.queryKey, - widget.queryFn, - initial: widget.initial, - retryConfig: widget.retryConfig, - refreshConfig: widget.refreshConfig, - jsonConfig: widget.jsonConfig, - ); - + _createQuery(); if (widget.onData != null) dataSubscription = query!.dataStream.listen(widget.onData); if (widget.onData != null) @@ -135,7 +138,7 @@ class _QueryBuilderState @override Widget build(BuildContext context) { if (query == null) { - return const SizedBox.shrink(); + _createQuery(); } return widget.builder(context, query!); } diff --git a/packages/fl_query_hooks/example/lib/pages/infinite_query.dart b/packages/fl_query_hooks/example/lib/pages/infinite_query.dart index 5f69bd9..c75f936 100644 --- a/packages/fl_query_hooks/example/lib/pages/infinite_query.dart +++ b/packages/fl_query_hooks/example/lib/pages/infinite_query.dart @@ -1,40 +1,61 @@ 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/fl_query.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:http/http.dart'; -class InfiniteQueryPageWidget extends StatefulWidget { +class InfiniteQueryPageWidget extends HookWidget { const InfiniteQueryPageWidget({super.key}); - @override - State createState() => - _InfiniteQueryPageWidgetState(); -} - -class _InfiniteQueryPageWidgetState extends State { - 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 Widget build(BuildContext context) { + final controller = useScrollController(); + final query = useInfiniteQuery( + "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( appBar: AppBar( title: const Text('Infinite Query'), @@ -48,58 +69,27 @@ class _InfiniteQueryPageWidgetState extends State { child: Text(query?.pages.length.toString() ?? "-69"), ); }), - body: InfiniteQueryBuilder( - "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(), - ), - builder: (context, query) { - final products = query.pages.map((e) => e.products).expand((e) => e); - if (!query.hasPages) { - return const Center( + body: !query.hasPages + ? const Center( child: CircularProgressIndicator(), - ); - } - return ListView( - controller: controller, - children: [ - for (final product in products) - ListTile( - title: Text(product.title), - subtitle: Text(product.description), - leading: Image.network(product.thumbnail), - ), - if (query.hasNextPage) - const Center( - child: CircularProgressIndicator(), - ), - if (query.hasErrors) - ...query.errors.map((e) => Text(e.message)).toList(), - ], - ); - }, - ), + ) + : ListView( + controller: controller, + children: [ + for (final product in products) + ListTile( + title: Text(product.title), + subtitle: Text(product.description), + leading: Image.network(product.thumbnail), + ), + if (query.hasNextPage) + const Center( + child: CircularProgressIndicator(), + ), + if (query.hasErrors) + ...query.errors.map((e) => Text(e.message)).toList(), + ], + ), ); } } diff --git a/packages/fl_query_hooks/lib/fl_query_hooks.dart b/packages/fl_query_hooks/lib/fl_query_hooks.dart index 6042eff..7093d9d 100644 --- a/packages/fl_query_hooks/lib/fl_query_hooks.dart +++ b/packages/fl_query_hooks/lib/fl_query_hooks.dart @@ -1,4 +1,5 @@ library fl_query_hooks; -export 'src/use_query.dart'; export 'src/use_query_client.dart'; +export 'src/use_query.dart'; +export 'src/use_infinite_query.dart'; diff --git a/packages/fl_query_hooks/lib/src/use_infinite_query.dart b/packages/fl_query_hooks/lib/src/use_infinite_query.dart new file mode 100644 index 0000000..c36e33e --- /dev/null +++ b/packages/fl_query_hooks/lib/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 + useInfiniteQuery( + String queryKey, + InfiniteQueryFn queryFn, { + required InfiniteQueryNextPage nextPage, + required PageType initialPage, + RetryConfig retryConfig = DefaultConstants.retryConfig, + RefreshConfig refreshConfig = DefaultConstants.refreshConfig, + JsonConfig? jsonConfig, + ValueChanged>? onData, + ValueChanged>? 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 + extends Hook> { + final InfiniteQueryFn queryFn; + final String queryKey; + + final PageType initialPage; + final InfiniteQueryNextPage nextPage; + + final RetryConfig retryConfig; + final RefreshConfig refreshConfig; + final JsonConfig? jsonConfig; + + final ValueChanged>? onData; + final ValueChanged>? 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(); +} + +class _UseInfiniteQueryState extends HookState< + InfiniteQuery, + UseInfiniteQuery> { + InfiniteQuery? query; + + VoidCallback? removeListener; + + StreamSubscription>? dataSubscription; + StreamSubscription>? errorSubscription; + + void rebuild([_]) { + setState(() {}); + } + + Future 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>( + 'queryFn', + hook.queryFn, + )); + properties + .add(DiagnosticsProperty>( + 'nextPage', + hook.nextPage, + )); + properties.add(DiagnosticsProperty( + 'initialPage', + hook.initialPage, + )); + properties.add(DiagnosticsProperty( + 'retryConfig', + hook.retryConfig, + )); + properties.add(DiagnosticsProperty( + 'refreshConfig', + hook.refreshConfig, + )); + properties.add(DiagnosticsProperty>( + 'jsonConfig', + hook.jsonConfig, + )); + properties + .add(DiagnosticsProperty>>( + 'onData', + hook.onData, + )); + properties + .add(DiagnosticsProperty>>( + 'onError', + hook.onError, + )); + properties.add(DiagnosticsProperty( + 'enabled', + hook.enabled, + )); + properties.add(DiagnosticsProperty( + 'queryKey', + hook.queryKey, + )); + } +} diff --git a/packages/fl_query_hooks/lib/src/use_query.dart b/packages/fl_query_hooks/lib/src/use_query.dart index 0d5502e..372512d 100644 --- a/packages/fl_query_hooks/lib/src/use_query.dart +++ b/packages/fl_query_hooks/lib/src/use_query.dart @@ -83,17 +83,20 @@ class _UseQueryState extends HookState< 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 initialize() async { setState(() { - query = QueryClient.of(context).createQuery( - hook.queryKey, - hook.queryFn, - initial: hook.initial, - retryConfig: hook.retryConfig, - refreshConfig: hook.refreshConfig, - jsonConfig: hook.jsonConfig, - ); - + _createQuery(); if (hook.onData != null) dataSubscription = query!.dataStream.listen(hook.onData); if (hook.onData != null) @@ -155,14 +158,7 @@ class _UseQueryState extends HookState< @override Query build(BuildContext context) { if (query == null) { - query = QueryClient.of(context).createQuery( - hook.queryKey, - hook.queryFn, - initial: hook.initial, - retryConfig: hook.retryConfig, - refreshConfig: hook.refreshConfig, - jsonConfig: hook.jsonConfig, - ); + _createQuery(); } return query!; }