From 04e8ce9c0a98ebe421a7093bc458359990c5d238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 23 Apr 2024 22:40:18 +0200 Subject: [PATCH] loadable state is now detecting device connection status on failure --- .../errorBar/error_bar_controller.dart | 26 ++++ .../errorBar/error_bar_state.dart | 11 ++ .../errorBar/error_bar_state.freezed.dart | 146 ++++++++++++++++++ .../background_loading_indicator.dart | 21 +++ lib/state/widgets/components/error_bar.dart | 53 +++++++ .../components/primary_loading_indicator.dart | 16 ++ .../widgets/loadable_controller_consumer.dart | 77 +++------ .../sub_selected_controller_consumer.dart | 6 +- lib/view/pages/more/test.dart | 2 +- pubspec.yaml | 1 + 10 files changed, 296 insertions(+), 63 deletions(-) create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_state.dart create mode 100644 lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart create mode 100644 lib/state/widgets/components/background_loading_indicator.dart create mode 100644 lib/state/widgets/components/error_bar.dart create mode 100644 lib/state/widgets/components/primary_loading_indicator.dart diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart new file mode 100644 index 0000000..b5a7858 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_controller.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +import '../../../../infrastructure/controller.dart'; +import 'error_bar_state.dart'; + +class ErrorBarController extends Controller { + late StreamSubscription> _updateStream; + + ErrorBarController() : super(const ErrorBarState(connections: null)) { + emitState(List v) => emit(state.copyWith(connections: v)); + + Connectivity().checkConnectivity().then(emitState); + _updateStream = Connectivity().onConnectivityChanged.listen(emitState); + } + + bool connectivityStatusKnown() => state.connections != null; + bool isConnected({bool? def}) => !(state.connections?.contains(ConnectivityResult.none) ?? def!); + + @override + Future close() { + _updateStream.cancel(); + return super.close(); + } +} diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart new file mode 100644 index 0000000..59a8b22 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_state.dart @@ -0,0 +1,11 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'error_bar_state.freezed.dart'; + +@freezed +class ErrorBarState with _$ErrorBarState { + const factory ErrorBarState({ + required List? connections, + }) = _ErrorBarState; +} diff --git a/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart b/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart new file mode 100644 index 0000000..2e60c16 --- /dev/null +++ b/lib/state/app/base/infrastructure/errorBar/error_bar_state.freezed.dart @@ -0,0 +1,146 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'error_bar_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ErrorBarState { + List? get connections => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ErrorBarStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ErrorBarStateCopyWith<$Res> { + factory $ErrorBarStateCopyWith( + ErrorBarState value, $Res Function(ErrorBarState) then) = + _$ErrorBarStateCopyWithImpl<$Res, ErrorBarState>; + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class _$ErrorBarStateCopyWithImpl<$Res, $Val extends ErrorBarState> + implements $ErrorBarStateCopyWith<$Res> { + _$ErrorBarStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_value.copyWith( + connections: freezed == connections + ? _value.connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ErrorBarStateImplCopyWith<$Res> + implements $ErrorBarStateCopyWith<$Res> { + factory _$$ErrorBarStateImplCopyWith( + _$ErrorBarStateImpl value, $Res Function(_$ErrorBarStateImpl) then) = + __$$ErrorBarStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List? connections}); +} + +/// @nodoc +class __$$ErrorBarStateImplCopyWithImpl<$Res> + extends _$ErrorBarStateCopyWithImpl<$Res, _$ErrorBarStateImpl> + implements _$$ErrorBarStateImplCopyWith<$Res> { + __$$ErrorBarStateImplCopyWithImpl( + _$ErrorBarStateImpl _value, $Res Function(_$ErrorBarStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? connections = freezed, + }) { + return _then(_$ErrorBarStateImpl( + connections: freezed == connections + ? _value._connections + : connections // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$ErrorBarStateImpl implements _ErrorBarState { + const _$ErrorBarStateImpl( + {required final List? connections}) + : _connections = connections; + + final List? _connections; + @override + List? get connections { + final value = _connections; + if (value == null) return null; + if (_connections is EqualUnmodifiableListView) return _connections; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'ErrorBarState(connections: $connections)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorBarStateImpl && + const DeepCollectionEquality() + .equals(other._connections, _connections)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_connections)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => + __$$ErrorBarStateImplCopyWithImpl<_$ErrorBarStateImpl>(this, _$identity); +} + +abstract class _ErrorBarState implements ErrorBarState { + const factory _ErrorBarState( + {required final List? connections}) = + _$ErrorBarStateImpl; + + @override + List? get connections; + @override + @JsonKey(ignore: true) + _$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state/widgets/components/background_loading_indicator.dart b/lib/state/widgets/components/background_loading_indicator.dart new file mode 100644 index 0000000..a7c4048 --- /dev/null +++ b/lib/state/widgets/components/background_loading_indicator.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class BackgroundLoadingIndicator extends StatelessWidget { + final bool visible; + const BackgroundLoadingIndicator({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(), + ); +} diff --git a/lib/state/widgets/components/error_bar.dart b/lib/state/widgets/components/error_bar.dart new file mode 100644 index 0000000..a4e6a9a --- /dev/null +++ b/lib/state/widgets/components/error_bar.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../../app/base/infrastructure/errorBar/error_bar_controller.dart'; +import '../../infrastructure/state_extensions.dart'; +import '../controller_provider.dart'; + +class ErrorBar extends StatelessWidget { + final bool visible; + const ErrorBar({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => ControllerProvider( + create: (context) => ErrorBarController(), + child: (context) => AnimatedSwitcher( + duration: animationDuration, + transitionBuilder: (Widget child, Animation animation) => SlideTransition( + position: Tween( + begin: const Offset(0.0, -1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + child: Visibility( + visible: visible, + child: Builder( + builder: (context) { + var controller = context.watchController(); + var status = controller.connectivityStatusKnown() && !controller.isConnected() + ? (icon: Icons.wifi_off_outlined, text: 'Offline', color: Colors.grey.shade600) + : (icon: Icons.wifi_find_outlined, text: 'Verbindung fehlgeschlagen', color: Theme.of(context).primaryColor); + + return Container( + height: 20, + decoration: BoxDecoration( + color: status.color, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(status.icon, size: 14), + const SizedBox(width: 10), + Text(status.text, style: const TextStyle(fontSize: 12)) + ], + ), + ); + }, + ) + ) + ), + ); +} diff --git a/lib/state/widgets/components/primary_loading_indicator.dart b/lib/state/widgets/components/primary_loading_indicator.dart new file mode 100644 index 0000000..4f21fc1 --- /dev/null +++ b/lib/state/widgets/components/primary_loading_indicator.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class PrimaryLoadingIndicator extends StatelessWidget { + final bool visible; + const PrimaryLoadingIndicator({required this.visible, super.key}); + + final Duration animationDuration = const Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) => AnimatedOpacity( + opacity: visible ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: const Center(child: CircularProgressIndicator()), + ); +} diff --git a/lib/state/widgets/loadable_controller_consumer.dart b/lib/state/widgets/loadable_controller_consumer.dart index 7ac0412..8fa0d33 100644 --- a/lib/state/widgets/loadable_controller_consumer.dart +++ b/lib/state/widgets/loadable_controller_consumer.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import '../infrastructure/controller.dart'; import '../infrastructure/loadable_state.dart'; import '../infrastructure/state_extensions.dart'; +import 'components/background_loading_indicator.dart'; +import 'components/error_bar.dart'; +import 'components/primary_loading_indicator.dart'; class LoadableControllerConsumer, TState extends LoadableState> extends StatelessWidget { final Widget child; @@ -13,69 +16,25 @@ class LoadableControllerConsumer, TState @override Widget build(BuildContext context) { var state = context.readController().state; - - var loadableContent = Stack( + return Column( children: [ - AnimatedOpacity( - opacity: !state.hasStateData() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: const Center(child: CircularProgressIndicator()), - ), - - AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: state.isBackgroundLoading() && !state.errorBarVisible() ? const LinearProgressIndicator() : const SizedBox.shrink(), - ), - - AnimatedOpacity( - opacity: state.hasStateData() ? 1.0 : 0.0, - duration: animationDuration, - curve: Curves.easeInOut, - child: state.hasStateData() ? child : const SizedBox.shrink() - ), - ], - ); - - var errorBar = AnimatedSwitcher( - duration: animationDuration, - transitionBuilder: (Widget child, Animation animation) => SlideTransition( - position: Tween( - begin: const Offset(0.0, -1.0), - end: Offset.zero, - ).animate(animation), - child: child, - ), - child: !state.errorBarVisible() - ? const SizedBox.shrink() - : Container( - height: 20, - decoration: const BoxDecoration( - color: Colors.red, - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.center, + ErrorBar(visible: state.errorBarVisible()), + Expanded( + child: Stack( children: [ - Icon(Icons.wifi_find_outlined, size: 12), - SizedBox(width: 10), - Text('Keine Verbindung', style: TextStyle(fontSize: 12)) + PrimaryLoadingIndicator(visible: !state.hasStateData()), + BackgroundLoadingIndicator(visible: state.isBackgroundLoading() && !state.errorBarVisible()), + + AnimatedOpacity( + opacity: state.hasStateData() ? 1.0 : 0.0, + duration: animationDuration, + curve: Curves.easeInOut, + child: state.hasStateData() ? child : const SizedBox.shrink() + ), ], ), - ), - ); - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [errorBar, loadableContent], + ) + ], ); } } diff --git a/lib/state/widgets/sub_selected_controller_consumer.dart b/lib/state/widgets/sub_selected_controller_consumer.dart index d57730b..fdbe68f 100644 --- a/lib/state/widgets/sub_selected_controller_consumer.dart +++ b/lib/state/widgets/sub_selected_controller_consumer.dart @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class SubSelectedControllerConsumer, TFullState, TFilteredState> extends StatelessWidget { final Widget Function(BuildContext context, TFilteredState state) child; - final TFilteredState Function(TFullState state) subselect; - const SubSelectedControllerConsumer({required this.subselect, required this.child, super.key}); + final TFilteredState Function(TFullState state) subSelect; + const SubSelectedControllerConsumer({required this.subSelect, required this.child, super.key}); @override - Widget build(BuildContext context) => BlocSelector(selector: subselect, builder: child); + Widget build(BuildContext context) => BlocSelector(selector: subSelect, builder: child); } diff --git a/lib/view/pages/more/test.dart b/lib/view/pages/more/test.dart index fb1966a..e6c69ad 100644 --- a/lib/view/pages/more/test.dart +++ b/lib/view/pages/more/test.dart @@ -37,7 +37,7 @@ class Test extends StatelessWidget { ), ControllerConsumer>(child: (context, state) => Text(state.data!.test.toString())), SubSelectedControllerConsumer, LoadingState>( - subselect: (state) => state.loadingState, + subSelect: (state) => state.loadingState, child: (context, state) => Text(state.toString()), ) ], diff --git a/pubspec.yaml b/pubspec.yaml index a97a3c9..e9de63c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dependencies: bloc: ^8.1.4 flutter_bloc: ^8.1.5 freezed_annotation: ^2.4.1 + connectivity_plus: ^6.0.3 dev_dependencies: flutter_test: