From 6c57faf7eb00742c4020b004b2614bc44956bf15 Mon Sep 17 00:00:00 2001 From: hectorAguero Date: Wed, 17 Apr 2024 17:10:15 -0400 Subject: [PATCH] Add AppAsyncSliverWidget and AppAsyncSliver classes --- lib/common_widgets/app_async_widget.dart | 55 ++++++++++++------ .../app_infinite_rotation_animation.dart | 58 +++++++++++++++++++ .../instruments/instruments_tab_page.dart | 2 +- lib/features/parades/parades_tab_page.dart | 7 ++- .../parades/parades_tab_providers.dart | 4 +- .../schools/widgets/schools_tab_body.dart | 2 +- 6 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 lib/common_widgets/app_infinite_rotation_animation.dart diff --git a/lib/common_widgets/app_async_widget.dart b/lib/common_widgets/app_async_widget.dart index b8ae59b..9b0a8a8 100644 --- a/lib/common_widgets/app_async_widget.dart +++ b/lib/common_widgets/app_async_widget.dart @@ -5,6 +5,7 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../extensions/app_localization_extension.dart'; import 'app_cupertino_button.dart'; +import 'app_infinite_rotation_animation.dart'; class AppAsyncWidget extends StatelessWidget { const AppAsyncWidget({ @@ -53,8 +54,8 @@ class AppAsyncWidget extends StatelessWidget { } } -class AppAsyncSliver extends StatelessWidget { - const AppAsyncSliver({ +class AppAsyncSliverWidget extends StatefulWidget { + const AppAsyncSliverWidget({ required this.asyncValue, required this.child, this.onErrorRetry, @@ -65,12 +66,31 @@ class AppAsyncSliver extends StatelessWidget { final VoidCallback? onErrorRetry; final Widget Function(T) child; + @override + State> createState() => + _AppAsyncSliverWidgetState(); +} + +class _AppAsyncSliverWidgetState extends State> { + bool isLoading = false; + @override Widget build(BuildContext context) { + final isRefreshing = widget.asyncValue.isRefreshing; return SliverAnimatedSwitcher( duration: kThemeAnimationDuration, - child: switch (asyncValue) { - AsyncData(:final value) => child(value), + child: switch (widget.asyncValue) { + AsyncData(:final value) => widget.child(value), + AsyncLoading() => const SliverFillRemaining( + key: ValueKey('loading'), + child: Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ), AsyncError(:final error) => SliverFillRemaining( key: const ValueKey('error'), child: Padding( @@ -84,13 +104,17 @@ class AppAsyncSliver extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), - if (onErrorRetry != null) + if (widget.onErrorRetry != null) Padding( padding: const EdgeInsets.only(top: 16), child: AppCupertinoButton.tinted( color: Theme.of(context).colorScheme.primary, - onPressed: onErrorRetry, - icon: const Icon(Icons.refresh), + onPressed: + !isRefreshing && !isLoading ? errorRetry : null, + icon: AppInfiniteRotationAnimation( + isLoading: isRefreshing || isLoading, + child: const Icon(Icons.refresh), + ), child: Text(context.loc.retry), ), ), @@ -99,17 +123,14 @@ class AppAsyncSliver extends StatelessWidget { ), ), ), - AsyncLoading() => const SliverFillRemaining( - key: ValueKey('loading'), - child: Center( - child: SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator.adaptive(), - ), - ), - ), }, ); } + + Future errorRetry() async { + setState(() => isLoading = true); + widget.onErrorRetry!(); + await Future.delayed(const Duration(milliseconds: 700)); + setState(() => isLoading = false); + } } diff --git a/lib/common_widgets/app_infinite_rotation_animation.dart b/lib/common_widgets/app_infinite_rotation_animation.dart new file mode 100644 index 0000000..d720711 --- /dev/null +++ b/lib/common_widgets/app_infinite_rotation_animation.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class AppInfiniteRotationAnimation extends StatefulWidget { + const AppInfiniteRotationAnimation({ + required this.isLoading, + required this.child, + this.duration = const Duration(milliseconds: 700), + super.key, + }); + final bool isLoading; + final Widget child; + final Duration duration; + + @override + State createState() => + _AppInfiniteRotationAnimationState(); +} + +class _AppInfiniteRotationAnimationState + extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration, + vsync: this, + ); + } + + @override + void didUpdateWidget(covariant AppInfiniteRotationAnimation oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isLoading != oldWidget.isLoading) { + if (widget.isLoading) { + _controller.repeat(); + } else { + _controller.animateTo(0).then((_) => _controller.stop()); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RotationTransition( + turns: _controller, + child: widget.child, + ); + } +} diff --git a/lib/features/instruments/instruments_tab_page.dart b/lib/features/instruments/instruments_tab_page.dart index c2584bd..acd2cb4 100644 --- a/lib/features/instruments/instruments_tab_page.dart +++ b/lib/features/instruments/instruments_tab_page.dart @@ -34,7 +34,7 @@ class InstrumentsTabPage extends ConsumerWidget { const SliverPadding(padding: EdgeInsets.only(top: 8)), SliverAnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: AppAsyncSliver( + child: AppAsyncSliverWidget( asyncValue: ref.watch(instrumentsTabProvider), onErrorRetry: () => ref.invalidate(instrumentsTabProvider), child: (value) => WebPaddingSliver.only( diff --git a/lib/features/parades/parades_tab_page.dart b/lib/features/parades/parades_tab_page.dart index 2d3bc7d..47f656a 100644 --- a/lib/features/parades/parades_tab_page.dart +++ b/lib/features/parades/parades_tab_page.dart @@ -70,9 +70,12 @@ class _ParadesTabPageState extends ConsumerState { largeTitle: context.loc.paradesTitle, ), ), - AppAsyncSliver( + AppAsyncSliverWidget( asyncValue: ref.watch(paradesProvider), - onErrorRetry: () => ref.invalidate(paradesProvider), + onErrorRetry: () async => + await Future.delayed(const Duration(milliseconds: 500), () { + ref.invalidate(paradesProvider); + }), child: (value) => SliverCrossAxisConstrained( maxCrossAxisExtent: ScreenSize.md.value, child: SuperSliverList.builder( diff --git a/lib/features/parades/parades_tab_providers.dart b/lib/features/parades/parades_tab_providers.dart index 32ddf1b..c68ba98 100644 --- a/lib/features/parades/parades_tab_providers.dart +++ b/lib/features/parades/parades_tab_providers.dart @@ -13,7 +13,9 @@ class Parades extends _$Parades { @override FutureOr> build() async { - return getParades(); + final parades = await getParades(); + + return parades; } Future fetchNextPage({int pageSize = _pageSize}) async { diff --git a/lib/features/schools/widgets/schools_tab_body.dart b/lib/features/schools/widgets/schools_tab_body.dart index 63f6915..1d1c844 100644 --- a/lib/features/schools/widgets/schools_tab_body.dart +++ b/lib/features/schools/widgets/schools_tab_body.dart @@ -21,7 +21,7 @@ class SchoolsTabBody extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return SliverSafeArea( top: false, - sliver: AppAsyncSliver( + sliver: AppAsyncSliverWidget( asyncValue: ref.watch(schoolsProvider), onErrorRetry: () => ref.invalidate(schoolsProvider), child: (value) => Consumer(