diff --git a/packages/fl_query/example/lib/main.dart b/packages/fl_query/example/lib/main.dart index 3a1596c..d668979 100644 --- a/packages/fl_query/example/lib/main.dart +++ b/packages/fl_query/example/lib/main.dart @@ -18,8 +18,12 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( + theme: ThemeData( + colorSchemeSeed: Colors.red[100], + useMaterial3: true, + ), title: 'FL Query Example', - showPerformanceOverlay: true, + // showPerformanceOverlay: true, routerConfig: router, ); } diff --git a/packages/fl_query/example/lib/pages/home.dart b/packages/fl_query/example/lib/pages/home.dart index 03632fd..1b5ae5c 100644 --- a/packages/fl_query/example/lib/pages/home.dart +++ b/packages/fl_query/example/lib/pages/home.dart @@ -20,6 +20,10 @@ class HomePage extends StatelessWidget { title: const Text('Infinite Query'), onTap: () => GoRouter.of(context).push('/infinite-query'), ), + ListTile( + title: const Text('Mutation'), + onTap: () => GoRouter.of(context).push('/mutation'), + ), ], ), ); diff --git a/packages/fl_query/example/lib/pages/mutation/mutation.dart b/packages/fl_query/example/lib/pages/mutation/mutation.dart new file mode 100644 index 0000000..fa98adc --- /dev/null +++ b/packages/fl_query/example/lib/pages/mutation/mutation.dart @@ -0,0 +1,130 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart'; + +class MutationPage extends StatefulWidget { + 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) { + return Scaffold( + appBar: AppBar( + title: const Text('Mutation'), + ), + body: MutationBuilder, dynamic, String, + Map, dynamic>( + const ValueKey('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 [ + ValueKey('hello'), + ], + builder: (context, mutation) { + if (mutation.hasData) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( + child: Text('Welcome ${mutation.data!['name']}'), + ), + const SizedBox(height: 16), + Center( + child: Text('Your email is ${mutation.data!['email']}'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + mutation.reset(); + }, + child: const Text('Log out'), + ), + ], + ); + } + + return 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: _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'), + ), + ], + ); + }, + ), + ); + } +} diff --git a/packages/fl_query/example/lib/router.dart b/packages/fl_query/example/lib/router.dart index d5103a9..a2cd1eb 100644 --- a/packages/fl_query/example/lib/router.dart +++ b/packages/fl_query/example/lib/router.dart @@ -1,5 +1,6 @@ import "package:example/pages/home.dart"; import "package:example/pages/infinite_query.dart"; +import "package:example/pages/mutation/mutation.dart"; import "package:example/pages/query.dart"; import "package:go_router/go_router.dart"; @@ -18,5 +19,9 @@ final router = GoRouter( path: '/infinite-query', builder: (context, state) => const InfiniteQueryPageWidget(), ), + GoRoute( + path: '/mutation', + builder: (context, state) => const MutationPage(), + ), ], ); diff --git a/packages/fl_query/lib/fl_query.dart b/packages/fl_query/lib/fl_query.dart index 38e84a6..b653265 100644 --- a/packages/fl_query/lib/fl_query.dart +++ b/packages/fl_query/lib/fl_query.dart @@ -10,8 +10,10 @@ export 'src/core/client.dart'; export 'src/core/infinite_query.dart'; export 'src/core/provider.dart'; export 'src/core/query.dart'; +export 'src/core/mutation.dart'; export 'src/widgets/query_builder.dart'; export 'src/widgets/query_listenable.dart'; export 'src/widgets/infinite_query_builder.dart'; export 'src/widgets/infinite_query_listenable.dart'; +export 'src/widgets/mutation_builder.dart'; diff --git a/packages/fl_query/lib/src/core/cache.dart b/packages/fl_query/lib/src/core/cache.dart index f0dd9c0..78894bc 100644 --- a/packages/fl_query/lib/src/core/cache.dart +++ b/packages/fl_query/lib/src/core/cache.dart @@ -3,13 +3,16 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:fl_query/src/collections/default_configs.dart'; import 'package:fl_query/src/core/infinite_query.dart'; +import 'package:fl_query/src/core/mutation.dart'; import 'package:fl_query/src/core/query.dart'; enum QueryCacheEventType { addQuery, addInfiniteQuery, + addMutation, removeQuery, removeInfiniteQuery, + removeMutation, } class QueryCacheEvent { @@ -22,6 +25,7 @@ class QueryCacheEvent { class QueryCache { final Set _queries; final Set _infiniteQueries; + final Set _mutations; final Duration cacheDuration; @@ -30,16 +34,41 @@ class QueryCache { QueryCache({ this.cacheDuration = DefaultConstants.cacheDuration, }) : _queries = Set(), - _infiniteQueries = Set() { + _infiniteQueries = Set(), + _mutations = Set() { Timer.periodic(cacheDuration, (timer) { - _queries.removeWhere((query) => query.isInactive); - _infiniteQueries.removeWhere((query) => query.isInactive); + _queries.removeWhere((query) { + if (query.isInactive) { + _eventController.add( + QueryCacheEvent(QueryCacheEventType.removeQuery, query), + ); + } + return query.isInactive; + }); + _infiniteQueries.removeWhere((query) { + if (query.isInactive) { + _eventController.add( + QueryCacheEvent(QueryCacheEventType.removeInfiniteQuery, query), + ); + } + return query.isInactive; + }); + _mutations.removeWhere((mutation) { + if (mutation.isInactive) { + _eventController.add( + QueryCacheEvent(QueryCacheEventType.removeMutation, mutation), + ); + } + return mutation.isInactive; + }); }); } UnmodifiableSetView get queries => UnmodifiableSetView(_queries); UnmodifiableSetView get infiniteQueries => UnmodifiableSetView(_infiniteQueries); + UnmodifiableSetView get mutations => + UnmodifiableSetView(_mutations); Stream get events => _eventController.stream; @@ -70,4 +99,18 @@ class QueryCache { QueryCacheEvent(QueryCacheEventType.removeInfiniteQuery, query), ); } + + void addMutation(Mutation mutation) { + _mutations.add(mutation); + _eventController.add( + QueryCacheEvent(QueryCacheEventType.addMutation, mutation), + ); + } + + void removeMutation(Mutation mutation) { + _mutations.remove(mutation); + _eventController.add( + QueryCacheEvent(QueryCacheEventType.removeMutation, mutation), + ); + } } diff --git a/packages/fl_query/lib/src/core/client.dart b/packages/fl_query/lib/src/core/client.dart index f25af16..e1f7908 100644 --- a/packages/fl_query/lib/src/core/client.dart +++ b/packages/fl_query/lib/src/core/client.dart @@ -5,6 +5,7 @@ import 'package:fl_query/src/collections/refresh_config.dart'; import 'package:fl_query/src/collections/retry_config.dart'; import 'package:fl_query/src/core/cache.dart'; import 'package:fl_query/src/core/infinite_query.dart'; +import 'package:fl_query/src/core/mutation.dart'; import 'package:fl_query/src/core/provider.dart'; import 'package:fl_query/src/core/query.dart'; import 'package:flutter/material.dart'; @@ -176,6 +177,60 @@ class QueryClient { .then((qs) => Map.fromEntries(qs)); } + Mutation + createMutation( + ValueKey key, + MutationFn mutationFn, { + RetryConfig retryConfig = DefaultConstants.retryConfig, + }) { + final mutation = cache.mutations + .firstWhere( + (query) => query.key == key, + orElse: () => Mutation( + key, + mutationFn, + retryConfig: retryConfig, + ), + ) + .cast(); + + mutation.updateMutationFn(mutationFn); + cache.addMutation(mutation); + return mutation; + } + + Future mutateMutation( + ValueKey key, + VariablesType variables, { + MutationFn? mutationFn, + RetryConfig retryConfig = DefaultConstants.retryConfig, + List refreshQueries = const [], + List refreshInfiniteQueries = const [], + }) async { + final mutation = getMutation( + key, + ) ?? + (mutationFn != null + ? createMutation( + key, + mutationFn, + retryConfig: retryConfig, + ) + : null); + final result = await mutation?.mutate(variables); + await this.refreshQueries(refreshQueries); + await refreshInfiniteQueriesAllPages(refreshInfiniteQueries); + return result; + } + + Mutation? + getMutation( + ValueKey key) { + return cache.mutations + .firstWhereOrNull((query) => query.key == key) + ?.cast(); + } + static QueryClient of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType()! diff --git a/packages/fl_query/lib/src/core/infinite_query.dart b/packages/fl_query/lib/src/core/infinite_query.dart index 30b2085..715b71f 100644 --- a/packages/fl_query/lib/src/core/infinite_query.dart +++ b/packages/fl_query/lib/src/core/infinite_query.dart @@ -202,7 +202,7 @@ class InfiniteQuery Future _operation(PageType page) { return _mutex.protect(() async { - retryOperation( + return await retryOperation( () => state.queryFn(page), config: retryConfig, onSuccessful: (data) async { @@ -219,20 +219,21 @@ class InfiniteQuery state = state.copyWith( pages: {...state.pages..remove(dataPage), dataPage}, ); - if (dataPage.data != null) + if (dataPage.data is DataType) { _dataController.add(PageEvent.fromPage(dataPage)); - if (jsonConfig != null) { - await _box.put( - key.value, - Map.fromEntries( - state.pages.map( - (e) => MapEntry( - e.page, - e.data != null ? jsonConfig!.toJson(e.data!) : null, + if (jsonConfig != null) { + await _box.put( + key.value, + Map.fromEntries( + state.pages.map( + (e) => MapEntry( + e.page, + e.data != null ? jsonConfig!.toJson(e.data!) : null, + ), ), ), - ), - ); + ); + } } }, onFailed: (error) { @@ -252,7 +253,7 @@ class InfiniteQuery errorPage, }, ); - if (errorPage.error != null) + if (errorPage.error is ErrorType) _errorController.add(PageEvent.fromPage(errorPage)); }, ); diff --git a/packages/fl_query/lib/src/core/mutation.dart b/packages/fl_query/lib/src/core/mutation.dart new file mode 100644 index 0000000..1d5c0b3 --- /dev/null +++ b/packages/fl_query/lib/src/core/mutation.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:fl_query/src/collections/default_configs.dart'; +import 'package:fl_query/src/collections/retry_config.dart'; +import 'package:fl_query/src/core/retryer.dart'; +import 'package:flutter/material.dart'; +import 'package:mutex/mutex.dart'; +import 'package:state_notifier/state_notifier.dart'; + +typedef MutationFn = Future Function( + VariablesType variables, +); + +class MutationState { + final DataType? data; + final ErrorType? error; + final MutationFn mutationFn; + final DateTime updatedAt; + + MutationState({ + required this.mutationFn, + this.data, + this.error, + DateTime? updatedAt, + }) : updatedAt = updatedAt ?? DateTime.now(); + + MutationState copyWith({ + DataType? data, + ErrorType? error, + DateTime? updatedAt, + MutationFn? mutationFn, + }) { + return MutationState( + mutationFn: mutationFn ?? this.mutationFn, + data: data ?? this.data, + error: error ?? this.error, + updatedAt: updatedAt ?? DateTime.now(), + ); + } +} + +class Mutation + extends StateNotifier> + with Retryer { + final ValueKey key; + final MutationFn mutationFn; + + final RetryConfig retryConfig; + + Mutation( + this.key, + this.mutationFn, { + this.retryConfig = DefaultConstants.retryConfig, + }) : _dataController = StreamController.broadcast(), + _errorController = StreamController.broadcast(), + _mutationController = StreamController.broadcast(), + super( + MutationState( + mutationFn: mutationFn, + ), + ); + + bool get isInactive => !hasListeners; + bool get isMutating => _mutex.isLocked; + bool get hasData => state.data != null; + bool get hasError => state.error != null; + + DataType? get data => state.data; + ErrorType? get error => state.error; + Stream get dataStream => _dataController.stream; + Stream get errorStream => _errorController.stream; + Stream get mutationStream => _mutationController.stream; + + final _mutex = Mutex(); + final StreamController _mutationController; + final StreamController _dataController; + final StreamController _errorController; + + Future _operate(VariablesType variables) { + return _mutex.protect(() async { + return await retryOperation( + () { + _mutationController.add(variables); + return state.mutationFn(variables); + }, + config: retryConfig, + onSuccessful: (data) { + state = state.copyWith(data: data); + if (data is DataType) { + _dataController.add(data); + } + }, + onFailed: (error) { + state = state.copyWith(error: error); + if (error is ErrorType) { + _errorController.add(error); + } + }, + ); + }); + } + + Future mutate( + VariablesType variables, { + bool scheduleToQueue = false, + }) { + if (isMutating && !scheduleToQueue) { + return Future.value(state.data); + } + return _operate(variables).then((_) => data); + } + + void updateMutationFn(MutationFn mutationFn) { + if (mutationFn == state.mutationFn) return; + state = state.copyWith(mutationFn: mutationFn, updatedAt: state.updatedAt); + } + + void reset() { + state = MutationState( + mutationFn: state.mutationFn, + ); + } + + @override + operator ==(Object other) { + return identical(this, other) || + (other is Mutation && key.value == other.key.value); + } + + @override + int get hashCode => key.hashCode; + + Mutation + cast() { + return this + as Mutation; + } +} diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index 56b0dbe..8e04c4a 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -118,7 +118,7 @@ class Query Future _operate() { return _mutex.protect(() async { - retryOperation( + return await retryOperation( state.queryFn, config: retryConfig, onSuccessful: (DataType? data) { @@ -126,17 +126,19 @@ class Query data: data, updatedAt: DateTime.now(), ); - if (data != null) _dataController.add(data); - if (jsonConfig != null && data != null) { - _box.put( - key.toString(), - jsonConfig!.toJson(data), - ); + if (data is DataType) { + _dataController.add(data); + if (jsonConfig != null) { + _box.put( + key.toString(), + jsonConfig!.toJson(data), + ); + } } }, onFailed: (ErrorType? error) { state = state.copyWith(error: error, updatedAt: DateTime.now()); - if (error != null) _errorController.add(error); + if (error is ErrorType) _errorController.add(error); }, ); }); diff --git a/packages/fl_query/lib/src/core/retryer.dart b/packages/fl_query/lib/src/core/retryer.dart index c00d026..71208ed 100644 --- a/packages/fl_query/lib/src/core/retryer.dart +++ b/packages/fl_query/lib/src/core/retryer.dart @@ -4,7 +4,7 @@ import 'package:fl_query/src/collections/retry_config.dart'; import 'package:flutter/material.dart'; mixin Retryer { - void retryOperation( + Future retryOperation( FutureOr Function() operation, { required RetryConfig config, required void Function(T?) onSuccessful, @@ -21,19 +21,19 @@ mixin Retryer { onSuccessful(result); break; } catch (e, stack) { - if (attempts == config.maxRetries - 1) { - if (e is E?) { + if (e is E?) { + if (attempts == config.maxRetries - 1) { onFailed(e as E?); - } else { - FlutterError.reportError( - FlutterErrorDetails( - exception: e, - library: 'fl_query', - context: ErrorDescription('retryOperation'), - stack: stack, - ), - ); } + } else { + FlutterError.reportError( + FlutterErrorDetails( + exception: e, + library: 'fl_query', + context: ErrorDescription('retryOperation'), + stack: stack, + ), + ); } } } 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 b2c302a..afec9a4 100644 --- a/packages/fl_query/lib/src/widgets/infinite_query_builder.dart +++ b/packages/fl_query/lib/src/widgets/infinite_query_builder.dart @@ -83,8 +83,10 @@ class _InfiniteQueryBuilderState jsonConfig: widget.jsonConfig, ); - dataSubscription = query!.dataStream.listen(widget.onData); - errorSubscription = query!.errorStream.listen(widget.onError); + if (widget.onData != null) + dataSubscription = query!.dataStream.listen(widget.onData); + if (widget.onError != null) + errorSubscription = query!.errorStream.listen(widget.onError); removeListener = query!.addListener(update); }); @@ -130,11 +132,13 @@ class _InfiniteQueryBuilderState } if (oldWidget.onData != widget.onData) { dataSubscription?.cancel(); - dataSubscription = query!.dataStream.listen(widget.onData); + if (widget.onData != null) + dataSubscription = query!.dataStream.listen(widget.onData); } if (oldWidget.onError != widget.onError) { errorSubscription?.cancel(); - errorSubscription = query!.errorStream.listen(widget.onError); + if (widget.onError != null) + errorSubscription = query!.errorStream.listen(widget.onError); } } diff --git a/packages/fl_query/lib/src/widgets/mutation_builder.dart b/packages/fl_query/lib/src/widgets/mutation_builder.dart new file mode 100644 index 0000000..6230ed3 --- /dev/null +++ b/packages/fl_query/lib/src/widgets/mutation_builder.dart @@ -0,0 +1,251 @@ +import 'dart:async'; + +import 'package:fl_query/src/collections/default_configs.dart'; +import 'package:fl_query/src/collections/retry_config.dart'; +import 'package:fl_query/src/core/client.dart'; +import 'package:fl_query/src/core/mutation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/diagnostics.dart'; + +typedef MutationBuilderFn = Widget + Function( + BuildContext context, + Mutation mutation, +); +typedef MutationOnDataFn = void Function( + DataType data, + RecoveryType? recoveryData, +); +typedef MutationOnErrorFn = void Function( + ErrorType error, + RecoveryType? recoveryData, +); +typedef MutationOnMutationFn + = FutureOr Function( + VariablesType variables, +); + +class MutationBuilder + extends StatefulWidget { + final MutationFn mutationFn; + final ValueKey mutationKey; + + final RetryConfig retryConfig; + + final MutationOnDataFn? onData; + final MutationOnErrorFn? onError; + final MutationOnMutationFn? onMutate; + + // widget specific + final MutationBuilderFn builder; + final List? refreshQueries; + final List? refreshInfiniteQueries; + + const MutationBuilder( + this.mutationKey, + this.mutationFn, { + required this.builder, + this.retryConfig = DefaultConstants.retryConfig, + this.onData, + this.onError, + this.onMutate, + this.refreshQueries, + this.refreshInfiniteQueries, + super.key, + }); + + @override + State< + MutationBuilder> createState() => _MutationBuilderState(); +} + +class _MutationBuilderState + extends State< + MutationBuilder> { + Mutation? mutation; + + VoidCallback? removeListener; + + StreamSubscription? mutationSubscription; + StreamSubscription? dataSubscription; + StreamSubscription? errorSubscription; + + RecoveryType? recoveryData; + + void update(_) { + if (mounted) { + setState(() {}); + } + } + + void subscribeOnMutate() { + if (widget.onMutate != null) + mutationSubscription = mutation!.mutationStream.listen( + (event) async { + recoveryData = await widget.onMutate?.call(event); + + if (widget.onData != null) { + dataSubscription?.cancel(); + subscribeOnData(); + } + + if (widget.onError != null) { + errorSubscription?.cancel(); + subscribeOnError(); + } + }, + ); + } + + void subscribeOnData() { + if (widget.onData != null || + widget.refreshInfiniteQueries != null || + widget.refreshQueries != null) + dataSubscription = mutation!.dataStream.listen( + (event) { + final data = widget.onData?.call(event, recoveryData); + if (widget.refreshQueries != null && mounted) { + QueryClient.of(context).refreshQueries(widget.refreshQueries!); + } + if (widget.refreshInfiniteQueries != null && mounted) { + QueryClient.of(context) + .refreshInfiniteQueries(widget.refreshInfiniteQueries!); + } + return data; + }, + ); + } + + void subscribeOnError() { + if (widget.onError != null) + errorSubscription = mutation!.errorStream.listen( + (event) { + return widget.onError?.call(event, recoveryData); + }, + ); + } + + Future initialize() async { + setState(() { + mutation = QueryClient.of(context).createMutation( + widget.mutationKey, + widget.mutationFn, + retryConfig: widget.retryConfig, + ); + subscribeOnMutate(); + subscribeOnData(); + subscribeOnError(); + removeListener = mutation!.addListener(update); + }); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await initialize(); + }); + } + + @override + void dispose() { + mutationSubscription?.cancel(); + dataSubscription?.cancel(); + errorSubscription?.cancel(); + removeListener?.call(); + super.dispose(); + } + + @override + void didUpdateWidget( + MutationBuilder + oldWidget, + ) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.mutationKey != widget.mutationKey) { + mutationSubscription?.cancel(); + dataSubscription?.cancel(); + errorSubscription?.cancel(); + removeListener?.call(); + initialize(); + return; + } + if (oldWidget.mutationFn != widget.mutationFn) { + mutation!.updateMutationFn(widget.mutationFn); + } + if (oldWidget.onMutate != widget.onMutate) { + mutationSubscription?.cancel(); + subscribeOnMutate(); + } + if (oldWidget.onData != widget.onData || + oldWidget.refreshQueries != widget.refreshQueries || + oldWidget.refreshInfiniteQueries != widget.refreshInfiniteQueries) { + dataSubscription?.cancel(); + subscribeOnData(); + mutationSubscription?.cancel(); + subscribeOnMutate(); + } + if (oldWidget.onError != widget.onError) { + errorSubscription?.cancel(); + subscribeOnError(); + } + } + + @override + Widget build(BuildContext context) { + if (mutation == null) { + return const SizedBox.shrink(); + } + return widget.builder(context, mutation!); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty< + Mutation>( + 'mutation', mutation), + ); + properties.add( + DiagnosticsProperty>('mutationKey', widget.mutationKey), + ); + properties.add( + DiagnosticsProperty>( + 'mutationFn', widget.mutationFn), + ); + properties.add( + DiagnosticsProperty('retryConfig', widget.retryConfig), + ); + properties.add( + DiagnosticsProperty>( + 'onData', + widget.onData, + ), + ); + properties.add( + DiagnosticsProperty>( + 'onError', + widget.onError, + ), + ); + properties.add( + DiagnosticsProperty>( + 'onMutation', + widget.onMutate, + ), + ); + properties.add( + DiagnosticsProperty< + MutationBuilderFn>( + 'builder', + widget.builder, + ), + ); + } +} diff --git a/packages/fl_query/lib/src/widgets/query_builder.dart b/packages/fl_query/lib/src/widgets/query_builder.dart index 8400e7e..be188f4 100644 --- a/packages/fl_query/lib/src/widgets/query_builder.dart +++ b/packages/fl_query/lib/src/widgets/query_builder.dart @@ -77,8 +77,10 @@ class _QueryBuilderState jsonConfig: widget.jsonConfig, ); - dataSubscription = query!.dataStream.listen(widget.onData); - errorSubscription = query!.errorStream.listen(widget.onError); + if (widget.onData != null) + dataSubscription = query!.dataStream.listen(widget.onData); + if (widget.onData != null) + errorSubscription = query!.errorStream.listen(widget.onError); removeListener = query!.addListener(update); }); @@ -123,11 +125,13 @@ class _QueryBuilderState } if (oldWidget.onData != widget.onData) { dataSubscription?.cancel(); - dataSubscription = query!.dataStream.listen(widget.onData); + if (widget.onData != null) + dataSubscription = query!.dataStream.listen(widget.onData); } if (oldWidget.onError != widget.onError) { errorSubscription?.cancel(); - errorSubscription = query!.errorStream.listen(widget.onError); + if (widget.onError != null) + errorSubscription = query!.errorStream.listen(widget.onError); } }