loadable state is now detecting device connection status on failure
This commit is contained in:
parent
450c26b187
commit
04e8ce9c0a
@ -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<ErrorBarState> {
|
||||
late StreamSubscription<List<ConnectivityResult>> _updateStream;
|
||||
|
||||
ErrorBarController() : super(const ErrorBarState(connections: null)) {
|
||||
emitState(List<ConnectivityResult> 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<void> close() {
|
||||
_updateStream.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -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<ConnectivityResult>? connections,
|
||||
}) = _ErrorBarState;
|
||||
}
|
@ -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>(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<ConnectivityResult>? get connections =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$ErrorBarStateCopyWith<ErrorBarState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ErrorBarStateCopyWith<$Res> {
|
||||
factory $ErrorBarStateCopyWith(
|
||||
ErrorBarState value, $Res Function(ErrorBarState) then) =
|
||||
_$ErrorBarStateCopyWithImpl<$Res, ErrorBarState>;
|
||||
@useResult
|
||||
$Res call({List<ConnectivityResult>? 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<ConnectivityResult>?,
|
||||
) 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<ConnectivityResult>? 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<ConnectivityResult>?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ErrorBarStateImpl implements _ErrorBarState {
|
||||
const _$ErrorBarStateImpl(
|
||||
{required final List<ConnectivityResult>? connections})
|
||||
: _connections = connections;
|
||||
|
||||
final List<ConnectivityResult>? _connections;
|
||||
@override
|
||||
List<ConnectivityResult>? 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<ConnectivityResult>? connections}) =
|
||||
_$ErrorBarStateImpl;
|
||||
|
||||
@override
|
||||
List<ConnectivityResult>? get connections;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$ErrorBarStateImplCopyWith<_$ErrorBarStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
@ -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<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
|
||||
);
|
||||
}
|
53
lib/state/widgets/components/error_bar.dart
Normal file
53
lib/state/widgets/components/error_bar.dart
Normal file
@ -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<ErrorBarController>(
|
||||
create: (context) => ErrorBarController(),
|
||||
child: (context) => AnimatedSwitcher(
|
||||
duration: animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
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<ErrorBarController>();
|
||||
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))
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
16
lib/state/widgets/components/primary_loading_indicator.dart
Normal file
16
lib/state/widgets/components/primary_loading_indicator.dart
Normal file
@ -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()),
|
||||
);
|
||||
}
|
@ -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<TController extends Controller<TState>, TState extends LoadableState> extends StatelessWidget {
|
||||
final Widget child;
|
||||
@ -13,69 +16,25 @@ class LoadableControllerConsumer<TController extends Controller<TState>, TState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var state = context.readController<TController>().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<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
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<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
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],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SubSelectedControllerConsumer<TController extends Cubit<TFullState>, 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<TController, TFullState, TFilteredState>(selector: subselect, builder: child);
|
||||
Widget build(BuildContext context) => BlocSelector<TController, TFullState, TFilteredState>(selector: subSelect, builder: child);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class Test extends StatelessWidget {
|
||||
),
|
||||
ControllerConsumer<MarianumMessageController, LoadableState<MarianumMessageState>>(child: (context, state) => Text(state.data!.test.toString())),
|
||||
SubSelectedControllerConsumer<MarianumMessageController, LoadableState<MarianumMessageState>, LoadingState>(
|
||||
subselect: (state) => state.loadingState,
|
||||
subSelect: (state) => state.loadingState,
|
||||
child: (context, state) => Text(state.toString()),
|
||||
)
|
||||
],
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user