diff --git a/packages/fl_query_hooks/example/lib/pages/home.dart b/packages/fl_query_hooks/example/lib/pages/home.dart index 1b5ae5c..a391347 100644 --- a/packages/fl_query_hooks/example/lib/pages/home.dart +++ b/packages/fl_query_hooks/example/lib/pages/home.dart @@ -8,7 +8,7 @@ class HomePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('FL Query Example'), + title: const Text('FL Query Hooks Example'), ), body: ListView( children: [ diff --git a/packages/fl_query_hooks/example/lib/pages/mutation/mutation.dart b/packages/fl_query_hooks/example/lib/pages/mutation/mutation.dart index 4e134b0..f31261b 100644 --- a/packages/fl_query_hooks/example/lib/pages/mutation/mutation.dart +++ b/packages/fl_query_hooks/example/lib/pages/mutation/mutation.dart @@ -1,65 +1,46 @@ -import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; -class MutationPage extends StatefulWidget { +class MutationPage extends HookWidget { const MutationPage({super.key}); - @override - State createState() => _MutationPageState(); -} - -class _MutationPageState extends State { - late TextEditingController _nameController; - late TextEditingController _emailController; - late TextEditingController _passwordController; - - @override - void initState() { - super.initState(); - _nameController = TextEditingController(); - _emailController = TextEditingController(); - _passwordController = TextEditingController(); - } - - @override - void dispose() { - _nameController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + final nameController = useTextEditingController(); + final emailController = useTextEditingController(); + final passwordController = useTextEditingController(); + + final mutation = useMutation, dynamic, + Map, dynamic>( + 'sign-up', + (variables) { + return Future.delayed( + const Duration(seconds: 1), + () => { + 'name': variables['name'], + 'email': variables['email'], + 'password': variables['password'], + }, + ); + }, + onMutate: (variables) { + print('onMutate: $variables'); + return "Recover ME"; + }, + onData: (data, recoveryData) { + print('onData: $data'); + print('recoveryData: $recoveryData'); + }, + refreshQueries: const ['hello'], + ); + return Scaffold( appBar: AppBar( title: const Text('Mutation'), ), - body: MutationBuilder, dynamic, Map, - dynamic>( - 'sign-up', - (variables) { - return Future.delayed( - const Duration(seconds: 1), - () => { - 'name': variables['name'], - 'email': variables['email'], - 'password': variables['password'], - }, - ); - }, - onMutate: (variables) { - print('onMutate: $variables'); - return "Recover ME"; - }, - onData: (data, recoveryData) { - print('onData: $data'); - print('recoveryData: $recoveryData'); - }, - refreshQueries: const ['hello'], - builder: (context, mutation) { - if (mutation.hasData) { - return ListView( + body: mutation.hasData + ? ListView( padding: const EdgeInsets.all(16), children: [ Center( @@ -77,52 +58,48 @@ class _MutationPageState extends State { child: const Text('Log out'), ), ], - ); - } - - return ListView( - padding: const EdgeInsets.all(16), - children: [ - TextField( - controller: _nameController, - keyboardType: TextInputType.name, - decoration: const InputDecoration( - labelText: 'Name', + ) + : ListView( + padding: const EdgeInsets.all(16), + children: [ + TextField( + controller: nameController, + keyboardType: TextInputType.name, + decoration: const InputDecoration( + labelText: 'Name', + ), ), - ), - const SizedBox(height: 16), - TextField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - labelText: 'Email', + const SizedBox(height: 16), + TextField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: 'Email', + ), ), - ), - const SizedBox(height: 16), - TextField( - controller: _passwordController, - obscureText: true, - decoration: const InputDecoration( - labelText: 'Password', + const SizedBox(height: 16), + TextField( + controller: passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Password', + ), ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - await mutation.mutate({ - 'name': _nameController.text, - 'email': _emailController.text, - 'password': _passwordController.text, - }); - }, - child: mutation.isMutating - ? const CircularProgressIndicator() - : const Text('Sign Up'), - ), - ], - ); - }, - ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await mutation.mutate({ + 'name': nameController.text, + 'email': emailController.text, + 'password': passwordController.text, + }); + }, + child: mutation.isMutating + ? const CircularProgressIndicator() + : const Text('Sign Up'), + ), + ], + ), ); } } diff --git a/packages/fl_query_hooks/lib/fl_query_hooks.dart b/packages/fl_query_hooks/lib/fl_query_hooks.dart index 7093d9d..deb579d 100644 --- a/packages/fl_query_hooks/lib/fl_query_hooks.dart +++ b/packages/fl_query_hooks/lib/fl_query_hooks.dart @@ -3,3 +3,4 @@ library fl_query_hooks; export 'src/use_query_client.dart'; export 'src/use_query.dart'; export 'src/use_infinite_query.dart'; +export 'src/use_mutation.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 index c36e33e..356ad57 100644 --- a/packages/fl_query_hooks/lib/src/use_infinite_query.dart +++ b/packages/fl_query_hooks/lib/src/use_infinite_query.dart @@ -1,11 +1,6 @@ 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:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -22,6 +17,7 @@ InfiniteQuery ValueChanged>? onData, ValueChanged>? onError, bool enabled = true, + List? keys, }) { return use(UseInfiniteQuery( queryKey, @@ -34,6 +30,7 @@ InfiniteQuery onData: onData, onError: onError, enabled: enabled, + keys: keys, )); } diff --git a/packages/fl_query_hooks/lib/src/use_mutation.dart b/packages/fl_query_hooks/lib/src/use_mutation.dart new file mode 100644 index 0000000..aba4f5c --- /dev/null +++ b/packages/fl_query_hooks/lib/src/use_mutation.dart @@ -0,0 +1,243 @@ +import 'dart:async'; + +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/diagnostics.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +Mutation + useMutation( + String mutationKey, + MutationFn mutationFn, { + RetryConfig retryConfig = DefaultConstants.retryConfig, + MutationOnDataFn? onData, + MutationOnErrorFn? onError, + MutationOnMutationFn? onMutate, + List? refreshQueries, + List? refreshInfiniteQueries, + List? keys, +}) { + return use( + UseMutation( + mutationKey, + mutationFn, + retryConfig: retryConfig, + onData: onData, + onError: onError, + onMutate: onMutate, + refreshQueries: refreshQueries, + refreshInfiniteQueries: refreshInfiniteQueries, + keys: keys, + ), + ); +} + +class UseMutation + extends Hook> { + final MutationFn mutationFn; + final String mutationKey; + + final RetryConfig retryConfig; + + final MutationOnDataFn? onData; + final MutationOnErrorFn? onError; + final MutationOnMutationFn? onMutate; + + // hook specific + final List? refreshQueries; + final List? refreshInfiniteQueries; + + const UseMutation( + this.mutationKey, + this.mutationFn, { + this.retryConfig = DefaultConstants.retryConfig, + this.onData, + this.onError, + this.onMutate, + this.refreshQueries, + this.refreshInfiniteQueries, + super.keys, + }); + + @override + createState() => + _UseMutationState(); +} + +class _UseMutationState + extends HookState, + UseMutation> { + Mutation? mutation; + + VoidCallback? removeListener; + + StreamSubscription? mutationSubscription; + StreamSubscription? dataSubscription; + StreamSubscription? errorSubscription; + + RecoveryType? recoveryData; + + void rebuild([_]) { + setState(() {}); + } + + void subscribeOnMutate() { + if (hook.onMutate != null) + mutationSubscription = mutation!.mutationStream.listen( + (event) async { + recoveryData = await hook.onMutate?.call(event); + + if (hook.onData != null) { + dataSubscription?.cancel(); + subscribeOnData(); + } + + if (hook.onError != null) { + errorSubscription?.cancel(); + subscribeOnError(); + } + }, + ); + } + + void subscribeOnData() { + if (hook.onData != null || + hook.refreshInfiniteQueries != null || + hook.refreshQueries != null) + dataSubscription = mutation!.dataStream.listen( + (event) { + final data = hook.onData?.call(event, recoveryData); + if (hook.refreshQueries != null) { + QueryClient.of(context).refreshQueries(hook.refreshQueries!); + } + if (hook.refreshInfiniteQueries != null) { + QueryClient.of(context) + .refreshInfiniteQueries(hook.refreshInfiniteQueries!); + } + return data; + }, + ); + } + + void subscribeOnError() { + if (hook.onError != null) + errorSubscription = mutation!.errorStream.listen( + (event) { + return hook.onError?.call(event, recoveryData); + }, + ); + } + + Future initialize() async { + setState(() { + _createMutation(); + subscribeOnMutate(); + subscribeOnData(); + subscribeOnError(); + removeListener = mutation!.addListener(rebuild); + }); + } + + void _createMutation() { + mutation = QueryClient.of(context).createMutation( + hook.mutationKey, + hook.mutationFn, + retryConfig: hook.retryConfig, + ); + } + + @override + void initHook() { + super.initHook(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await initialize(); + }); + } + + @override + void dispose() { + mutationSubscription?.cancel(); + dataSubscription?.cancel(); + errorSubscription?.cancel(); + removeListener?.call(); + super.dispose(); + } + + @override + void didUpdateHook(oldHook) { + super.didUpdateHook(oldHook); + + if (oldHook.mutationKey != hook.mutationKey) { + mutationSubscription?.cancel(); + dataSubscription?.cancel(); + errorSubscription?.cancel(); + removeListener?.call(); + initialize(); + return; + } + if (oldHook.mutationFn != hook.mutationFn) { + mutation!.updateMutationFn(hook.mutationFn); + } + if (oldHook.onMutate != hook.onMutate) { + mutationSubscription?.cancel(); + subscribeOnMutate(); + } + if (oldHook.onData != hook.onData || + oldHook.refreshQueries != hook.refreshQueries || + oldHook.refreshInfiniteQueries != hook.refreshInfiniteQueries) { + dataSubscription?.cancel(); + subscribeOnData(); + mutationSubscription?.cancel(); + subscribeOnMutate(); + } + if (oldHook.onError != hook.onError) { + errorSubscription?.cancel(); + subscribeOnError(); + } + } + + @override + build(BuildContext context) { + if (mutation == null) { + _createMutation(); + } + return mutation!; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty>( + 'mutation', mutation), + ); + properties.add( + DiagnosticsProperty('mutationKey', hook.mutationKey), + ); + properties.add( + DiagnosticsProperty>( + 'mutationFn', hook.mutationFn), + ); + properties.add( + DiagnosticsProperty('retryConfig', hook.retryConfig), + ); + properties.add( + DiagnosticsProperty>( + 'onData', + hook.onData, + ), + ); + properties.add( + DiagnosticsProperty>( + 'onError', + hook.onError, + ), + ); + properties.add( + DiagnosticsProperty>( + 'onMutation', + hook.onMutate, + ), + ); + } +}