diff --git a/packages/fl_query/example/lib/main.dart b/packages/fl_query/example/lib/main.dart index 408ee41..dbbae92 100644 --- a/packages/fl_query/example/lib/main.dart +++ b/packages/fl_query/example/lib/main.dart @@ -28,7 +28,14 @@ class MainApp extends StatelessWidget { ), title: 'FL Query Example', builder: (context, child) { - return FlQueryDevtools(child: child); + return FlQueryDevtools( + child: QueryStateResolverProvider( + child: child!, + offline: () => const Center( + child: Text("Why u offline? How can u live?"), + ), + ), + ); }, routerConfig: router, ); diff --git a/packages/fl_query/example/lib/pages/infinite_query.dart b/packages/fl_query/example/lib/pages/infinite_query.dart index 171d44b..a6d8589 100644 --- a/packages/fl_query/example/lib/pages/infinite_query.dart +++ b/packages/fl_query/example/lib/pages/infinite_query.dart @@ -67,33 +67,40 @@ class _InfiniteQueryPageWidgetState extends State { return lastPage + 1; }, initialPage: 0, - jsonConfig: JsonConfig( - fromJson: (json) => PagedProducts.fromJson(json), - toJson: (data) => data.toJson(), - ), + // 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( + + return query.resolve( + (data) => 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(), + ], + ), + loading: () => 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(), - ], + ), + error: (error) => const Center( + child: Text("Sorry! There was an error :'("), + ), + offline: () => const Center( + child: Text("Yo. You're offline. Are you in a jungle?"), + ), ); }, ), diff --git a/packages/fl_query/example/lib/pages/query.dart b/packages/fl_query/example/lib/pages/query.dart index c4b8a4f..f4ff629 100644 --- a/packages/fl_query/example/lib/pages/query.dart +++ b/packages/fl_query/example/lib/pages/query.dart @@ -43,17 +43,11 @@ class QueryPage extends StatelessWidget { debugPrint('onError: $error'); }, builder: (context, query) { - if (query.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (query.hasError) { - return Center( - child: Text(query.error.toString()), - ); - } - return Center( - child: Text(query.data ?? "Unfortunately, there's no data"), + return query.resolveWith( + context, + (data) => Center(child: Text(data)), + error: (error) => Center(child: Text(error.toString())), + loading: () => const Center(child: CircularProgressIndicator()), ); }, ), diff --git a/packages/fl_query/lib/fl_query.dart b/packages/fl_query/lib/fl_query.dart index 0f88c16..6e67091 100644 --- a/packages/fl_query/lib/fl_query.dart +++ b/packages/fl_query/lib/fl_query.dart @@ -18,5 +18,8 @@ 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'; +export 'src/widgets/state_resolvers/query_state.dart'; +export 'src/widgets/state_resolvers/infinite_query_state.dart'; +export 'src/widgets/state_resolvers/mutation_state.dart'; export 'src/devtools/devtools.dart'; diff --git a/packages/fl_query/lib/src/collections/connectivity_adapter.dart b/packages/fl_query/lib/src/collections/connectivity_adapter.dart index 77e1738..e624bb2 100644 --- a/packages/fl_query/lib/src/collections/connectivity_adapter.dart +++ b/packages/fl_query/lib/src/collections/connectivity_adapter.dart @@ -1,4 +1,15 @@ abstract class ConnectivityAdapter { + bool _isConnectedSync; + + ConnectivityAdapter() : _isConnectedSync = true { + isConnected.then((c) => _isConnectedSync = c); + onConnectivityChanged.listen((isConnected) { + _isConnectedSync = isConnected; + }); + } + + bool get isConnectedSync => _isConnectedSync; + Future get isConnected; Stream get onConnectivityChanged; diff --git a/packages/fl_query/lib/src/core/infinite_query.dart b/packages/fl_query/lib/src/core/infinite_query.dart index 57a4624..324d06c 100644 --- a/packages/fl_query/lib/src/core/infinite_query.dart +++ b/packages/fl_query/lib/src/core/infinite_query.dart @@ -2,10 +2,11 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; -import 'package:fl_query/fl_query.dart'; +import 'package:fl_query/src/core/client.dart'; import 'package:fl_query/src/core/mixins/retryer.dart'; import 'package:fl_query/src/core/mixins/validation.dart'; -import 'package:flutter/material.dart' hide Listener; +import 'package:fl_query/src/widgets/state_resolvers/infinite_query_state.dart'; +import 'package:flutter/widgets.dart' hide Listener; import 'package:hive_flutter/adapters.dart'; import 'package:mutex/mutex.dart'; import 'package:state_notifier/state_notifier.dart'; @@ -251,7 +252,11 @@ class InfiniteQuery bool get hasNextPage => getNextPage != null; - Future _operate(PageType page) { + Future _operate(PageType page) async { + if (!await QueryClient.connectivity.isConnected && + retryConfig.cancelWhenOffline) { + return; + } return _mutex.protect(() async { state = state.copyWith(); _operation = cancellableRetryOperation( @@ -409,6 +414,52 @@ class InfiniteQuery ); } + Widget resolve( + Widget Function(List data) data, { + required Widget Function(List errors) error, + required Widget Function() loading, + Widget Function()? offline, + }) { + if (hasPages) { + return data(this.pages); + } else if (!QueryClient.connectivity.isConnectedSync) { + return offline != null ? offline() : loading(); + } else if (hasErrors) { + return error(this.errors); + } else { + return loading(); + } + } + + Widget resolveWith( + BuildContext context, + Widget Function(List data) data, { + required Widget Function(List error)? error, + required Widget Function()? loading, + Widget Function()? offline, + }) { + final resolvents = InfiniteQueryStateResolverProvider.of(context); + + assert( + resolvents.error != null || error != null, + 'You must provide an error widget or an error resolver using `InfiniteQueryStateResolverProvider`', + ); + + assert( + resolvents.loading != null || loading != null, + 'You must provide a loading widget or a loading resolver using `InfiniteQueryStateResolverProvider`', + ); + + return resolve( + data, + error: resolvents.error != null + ? (e) => resolvents.error!(e.cast()) + : error!, + loading: (resolvents.loading ?? loading)!, + offline: resolvents.offline ?? offline, + ); + } + /// Reset all data, pages, error, events of this query /// /// This will also remove every data of this [InfiniteQuery] from diff --git a/packages/fl_query/lib/src/core/mutation.dart b/packages/fl_query/lib/src/core/mutation.dart index 02f6417..8cdf9bf 100644 --- a/packages/fl_query/lib/src/core/mutation.dart +++ b/packages/fl_query/lib/src/core/mutation.dart @@ -4,6 +4,8 @@ import 'package:async/async.dart'; import 'package:fl_query/src/collections/retry_config.dart'; import 'package:fl_query/src/core/client.dart'; import 'package:fl_query/src/core/mixins/retryer.dart'; +import 'package:fl_query/src/widgets/state_resolvers/mutation_state.dart'; +import 'package:flutter/widgets.dart'; import 'package:mutex/mutex.dart'; import 'package:state_notifier/state_notifier.dart'; @@ -88,7 +90,11 @@ class Mutation Stream get errorStream => _errorController.stream; Stream get mutationStream => _mutationController.stream; - Future _operate(VariablesType variables) { + Future _operate(VariablesType variables) async { + if (!await QueryClient.connectivity.isConnected && + retryConfig.cancelWhenOffline) { + return; + } return _mutex.protect(() async { state = state.copyWith(); _operation = cancellableRetryOperation( @@ -128,6 +134,57 @@ class Mutation _mutationFn = mutationFn; } + Widget resolve( + Widget Function(DataType data) data, { + required Widget Function(ErrorType error) error, + required Widget Function() loading, + Widget Function()? mutating, + Widget Function()? offline, + }) { + if (hasData) { + return data(this.data!); + } else if (!QueryClient.connectivity.isConnectedSync) { + return offline != null ? offline() : loading(); + } else if (isMutating) { + return mutating != null ? mutating() : loading(); + } else if (hasError) { + return error(this.error!); + } else { + return loading(); + } + } + + Widget resolveWith( + BuildContext context, + Widget Function(DataType data) data, { + required Widget Function(ErrorType error)? error, + required Widget Function()? loading, + Widget Function()? offline, + Widget Function()? mutating, + }) { + final resolvents = MutationStateResolverProvider.of(context); + + assert( + resolvents.error != null || error != null, + 'You must provide an error widget or an error resolver using `MutationStateResolverProvider`', + ); + + assert( + resolvents.loading != null || loading != null, + 'You must provide a loading widget or a loading resolver using `MutationStateResolverProvider`', + ); + + return resolve( + data, + error: resolvents.error != null + ? (e) => resolvents.error!(e as dynamic) + : error!, + loading: (resolvents.loading ?? loading)!, + offline: resolvents.offline ?? offline, + mutating: resolvents.mutating ?? mutating, + ); + } + Future reset() async { await _operation?.cancel(); state = MutationState(); diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index c4ad98c..d45e405 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -6,6 +6,8 @@ import 'package:fl_query/src/collections/retry_config.dart'; import 'package:fl_query/src/core/client.dart'; import 'package:fl_query/src/core/mixins/retryer.dart'; import 'package:fl_query/src/core/mixins/validation.dart'; +import 'package:fl_query/src/widgets/state_resolvers/query_state.dart'; +import 'package:flutter/widgets.dart' hide Listener; import 'package:hive_flutter/adapters.dart'; import 'package:mutex/mutex.dart'; import 'package:state_notifier/state_notifier.dart'; @@ -137,7 +139,11 @@ class Query CancelableOperation? _operation; - Future _operate() { + Future _operate() async { + if (!await QueryClient.connectivity.isConnected && + retryConfig.cancelWhenOffline) { + return; + } return _mutex.protect(() async { state = state.copyWith(); _operation = cancellableRetryOperation( @@ -196,6 +202,52 @@ class Query _box.delete(key); } + Widget resolve( + Widget Function(DataType data) data, { + required Widget Function(ErrorType data) error, + required Widget Function() loading, + Widget Function()? offline, + }) { + if (hasData) { + return data(this.data!); + } else if (!QueryClient.connectivity.isConnectedSync) { + return offline != null ? offline() : loading(); + } else if (hasError) { + return error(this.error!); + } else { + return loading(); + } + } + + Widget resolveWith( + BuildContext context, + Widget Function(DataType data) data, { + required Widget Function(ErrorType error)? error, + required Widget Function()? loading, + Widget Function()? offline, + }) { + final resolvents = QueryStateResolverProvider.of(context); + + assert( + resolvents.error != null || error != null, + 'You must provide an error widget or an error resolver using `QueryStateResolverProvider`', + ); + + assert( + resolvents.loading != null || loading != null, + 'You must provide a loading widget or a loading resolver using `QueryStateResolverProvider`', + ); + + return resolve( + data, + error: resolvents.error != null + ? (e) => resolvents.error!(e as dynamic) + : error!, + loading: (resolvents.loading ?? loading)!, + offline: resolvents.offline ?? offline, + ); + } + @override RemoveListener addListener(Listener> listener, {bool fireImmediately = true}) { diff --git a/packages/fl_query/lib/src/widgets/state_resolvers/infinite_query_state.dart b/packages/fl_query/lib/src/widgets/state_resolvers/infinite_query_state.dart new file mode 100644 index 0000000..e26fbab --- /dev/null +++ b/packages/fl_query/lib/src/widgets/state_resolvers/infinite_query_state.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +class InfiniteQueryStateResolverProvider extends InheritedWidget { + final Widget Function(List errors)? error; + final Widget Function()? loading; + final Widget Function()? offline; + + InfiniteQueryStateResolverProvider({ + this.error, + this.loading, + this.offline, + required super.child, + }) : assert( + error != null || loading != null || offline != null, + 'Why are you using `InfiniteQueryStateResolverProvider` with no resolver?\n' + 'You should provide at least one resolver.', + ); + + static InfiniteQueryStateResolverProvider? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType< + InfiniteQueryStateResolverProvider>(); + } + + static InfiniteQueryStateResolverProvider of(BuildContext context) { + final provider = maybeOf(context); + assert( + provider != null, + 'You are trying to access a `InfiniteQueryStateResolverProvider` outside of its scope.\n' + 'You should wrap your widget tree with `InfiniteQueryStateResolverProvider`.', + ); + return provider!; + } + + @override + bool updateShouldNotify(InfiniteQueryStateResolverProvider oldWidget) { + return error != oldWidget.error || + loading != oldWidget.loading || + offline != oldWidget.offline; + } +} diff --git a/packages/fl_query/lib/src/widgets/state_resolvers/mutation_state.dart b/packages/fl_query/lib/src/widgets/state_resolvers/mutation_state.dart new file mode 100644 index 0000000..a23d349 --- /dev/null +++ b/packages/fl_query/lib/src/widgets/state_resolvers/mutation_state.dart @@ -0,0 +1,43 @@ +import 'package:flutter/widgets.dart'; + +class MutationStateResolverProvider extends InheritedWidget { + final Widget Function(dynamic error)? error; + final Widget Function()? loading; + final Widget Function()? offline; + final Widget Function()? mutating; + + MutationStateResolverProvider({ + this.error, + this.loading, + this.offline, + this.mutating, + required super.child, + }) : assert( + error != null || loading != null || offline != null, + 'Why are you using `MutationStateResolverProvider` with no resolver?\n' + 'You should provide at least one resolver.', + ); + + static MutationStateResolverProvider? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType(); + } + + static MutationStateResolverProvider of(BuildContext context) { + final provider = maybeOf(context); + assert( + provider != null, + 'You are trying to access a `MutationStateResolverProvider` outside of its scope.\n' + 'You should wrap your widget tree with `MutationStateResolverProvider`.', + ); + return provider!; + } + + @override + bool updateShouldNotify(MutationStateResolverProvider oldWidget) { + return error != oldWidget.error || + loading != oldWidget.loading || + offline != oldWidget.offline || + mutating != oldWidget.mutating; + } +} diff --git a/packages/fl_query/lib/src/widgets/state_resolvers/query_state.dart b/packages/fl_query/lib/src/widgets/state_resolvers/query_state.dart new file mode 100644 index 0000000..69632e7 --- /dev/null +++ b/packages/fl_query/lib/src/widgets/state_resolvers/query_state.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +class QueryStateResolverProvider extends InheritedWidget { + final Widget Function(dynamic error)? error; + final Widget Function()? loading; + final Widget Function()? offline; + + QueryStateResolverProvider({ + this.error, + this.loading, + this.offline, + required super.child, + }) : assert( + error != null || loading != null || offline != null, + 'Why are you using `QueryStateResolverProvider` with no resolver?\n' + 'You should provide at least one resolver.', + ); + + static QueryStateResolverProvider? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType(); + } + + static QueryStateResolverProvider of(BuildContext context) { + final provider = maybeOf(context); + assert( + provider != null, + 'You are trying to access a `QueryStateResolverProvider` outside of its scope.\n' + 'You should wrap your widget tree with `QueryStateResolverProvider`.', + ); + return provider!; + } + + @override + bool updateShouldNotify(QueryStateResolverProvider oldWidget) { + return error != oldWidget.error || + loading != oldWidget.loading || + offline != oldWidget.offline; + } +} diff --git a/packages/fl_query/pubspec.yaml b/packages/fl_query/pubspec.yaml index 4c108e4..1f1010d 100644 --- a/packages/fl_query/pubspec.yaml +++ b/packages/fl_query/pubspec.yaml @@ -8,7 +8,7 @@ issue_tracker: https://github.com/KRTirtho/fl-query/issues repository: https://github.com/KRTirtho/fl-query environment: - sdk: ">=2.17.1 <3.0.0" + sdk: ">=2.17.1 <4.0.0" flutter: ">=1.17.0" dependencies: