loading state and error handling refactor
This commit is contained in:
@@ -41,6 +41,12 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
|
||||
? Colors.grey.shade600
|
||||
: Theme.of(context).primaryColor;
|
||||
|
||||
Color connectionForegroundColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
|
||||
? Colors.white
|
||||
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
|
||||
String connectionText({int? lastUpdated}) => connectivityStatusKnown()
|
||||
? isConnected()
|
||||
? 'Verbindung fehlgeschlagen'
|
||||
|
||||
@@ -6,6 +6,7 @@ part 'loading_error.freezed.dart';
|
||||
abstract class LoadingError with _$LoadingError {
|
||||
const factory LoadingError({
|
||||
required String message,
|
||||
String? technicalDetails,
|
||||
@Default(false) bool allowRetry,
|
||||
}) = _LoadingError;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$LoadingError {
|
||||
|
||||
String get message; bool get allowRetry;
|
||||
String get message; String? get technicalDetails; bool get allowRetry;
|
||||
/// Create a copy of LoadingError
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,16 +25,16 @@ $LoadingErrorCopyWith<LoadingError> get copyWith => _$LoadingErrorCopyWithImpl<L
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.technicalDetails, technicalDetails) || other.technicalDetails == technicalDetails)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,allowRetry);
|
||||
int get hashCode => Object.hash(runtimeType,message,technicalDetails,allowRetry);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadingError(message: $message, allowRetry: $allowRetry)';
|
||||
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ abstract mixin class $LoadingErrorCopyWith<$Res> {
|
||||
factory $LoadingErrorCopyWith(LoadingError value, $Res Function(LoadingError) _then) = _$LoadingErrorCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message, bool allowRetry
|
||||
String message, String? technicalDetails, bool allowRetry
|
||||
});
|
||||
|
||||
|
||||
@@ -62,10 +62,11 @@ class _$LoadingErrorCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of LoadingError
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? allowRetry = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? technicalDetails = freezed,Object? allowRetry = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,allowRetry: null == allowRetry ? _self.allowRetry : allowRetry // ignore: cast_nullable_to_non_nullable
|
||||
as String,technicalDetails: freezed == technicalDetails ? _self.technicalDetails : technicalDetails // ignore: cast_nullable_to_non_nullable
|
||||
as String?,allowRetry: null == allowRetry ? _self.allowRetry : allowRetry // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
@@ -151,10 +152,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String message, bool allowRetry)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String message, String? technicalDetails, bool allowRetry)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadingError() when $default != null:
|
||||
return $default(_that.message,_that.allowRetry);case _:
|
||||
return $default(_that.message,_that.technicalDetails,_that.allowRetry);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -172,10 +173,10 @@ return $default(_that.message,_that.allowRetry);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String message, bool allowRetry) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String message, String? technicalDetails, bool allowRetry) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadingError():
|
||||
return $default(_that.message,_that.allowRetry);case _:
|
||||
return $default(_that.message,_that.technicalDetails,_that.allowRetry);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -192,10 +193,10 @@ return $default(_that.message,_that.allowRetry);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String message, bool allowRetry)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String message, String? technicalDetails, bool allowRetry)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadingError() when $default != null:
|
||||
return $default(_that.message,_that.allowRetry);case _:
|
||||
return $default(_that.message,_that.technicalDetails,_that.allowRetry);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -207,10 +208,11 @@ return $default(_that.message,_that.allowRetry);case _:
|
||||
|
||||
|
||||
class _LoadingError implements LoadingError {
|
||||
const _LoadingError({required this.message, this.allowRetry = false});
|
||||
const _LoadingError({required this.message, this.technicalDetails, this.allowRetry = false});
|
||||
|
||||
|
||||
@override final String message;
|
||||
@override final String? technicalDetails;
|
||||
@override@JsonKey() final bool allowRetry;
|
||||
|
||||
/// Create a copy of LoadingError
|
||||
@@ -223,16 +225,16 @@ _$LoadingErrorCopyWith<_LoadingError> get copyWith => __$LoadingErrorCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.technicalDetails, technicalDetails) || other.technicalDetails == technicalDetails)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,allowRetry);
|
||||
int get hashCode => Object.hash(runtimeType,message,technicalDetails,allowRetry);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadingError(message: $message, allowRetry: $allowRetry)';
|
||||
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +245,7 @@ abstract mixin class _$LoadingErrorCopyWith<$Res> implements $LoadingErrorCopyWi
|
||||
factory _$LoadingErrorCopyWith(_LoadingError value, $Res Function(_LoadingError) _then) = __$LoadingErrorCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String message, bool allowRetry
|
||||
String message, String? technicalDetails, bool allowRetry
|
||||
});
|
||||
|
||||
|
||||
@@ -260,10 +262,11 @@ class __$LoadingErrorCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of LoadingError
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? message = null,Object? allowRetry = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? message = null,Object? technicalDetails = freezed,Object? allowRetry = null,}) {
|
||||
return _then(_LoadingError(
|
||||
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,allowRetry: null == allowRetry ? _self.allowRetry : allowRetry // ignore: cast_nullable_to_non_nullable
|
||||
as String,technicalDetails: freezed == technicalDetails ? _self.technicalDetails : technicalDetails // ignore: cast_nullable_to_non_nullable
|
||||
as String?,allowRetry: null == allowRetry ? _self.allowRetry : allowRetry // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
visible: showErrorBar,
|
||||
hasContent: hasContent,
|
||||
message: loadableState.error?.message,
|
||||
technicalDetails: loadableState.error?.technicalDetails,
|
||||
lastUpdated: loadableState.lastFetch,
|
||||
),
|
||||
Expanded(
|
||||
@@ -95,7 +96,11 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
children: [
|
||||
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
|
||||
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
|
||||
LoadableStateErrorScreen(visible: showError, message: loadableState.error?.message),
|
||||
LoadableStateErrorScreen(
|
||||
visible: showError,
|
||||
message: loadableState.error?.message,
|
||||
technicalDetails: loadableState.error?.technicalDetails,
|
||||
),
|
||||
|
||||
AnimatedOpacity(
|
||||
opacity: hasContent ? 1.0 : 0.0,
|
||||
|
||||
@@ -10,11 +10,13 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
final bool visible;
|
||||
final bool hasContent;
|
||||
final String? message;
|
||||
final String? technicalDetails;
|
||||
final int? lastUpdated;
|
||||
const LoadableStateErrorBar({
|
||||
required this.visible,
|
||||
this.hasContent = false,
|
||||
this.message,
|
||||
this.technicalDetails,
|
||||
this.lastUpdated,
|
||||
super.key,
|
||||
});
|
||||
@@ -48,7 +50,12 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if(!bloc.isConnected()) return;
|
||||
InfoDialog.show(context, 'Exception: ${message.toString()}');
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null && technicalDetails!.isNotEmpty) technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(context, body);
|
||||
},
|
||||
child: Container(
|
||||
height: 20,
|
||||
@@ -85,13 +92,17 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
final foreground = bloc.connectionForegroundColor(context);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 14),
|
||||
Icon(bloc.connectionIcon(), size: 14, color: foreground),
|
||||
const SizedBox(width: 10),
|
||||
Text(bloc.connectionText(lastUpdated: widget.lastUpdated), style: const TextStyle(fontSize: 12))
|
||||
Text(
|
||||
bloc.connectionText(lastUpdated: widget.lastUpdated),
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../../widget/info_dialog.dart';
|
||||
import '../bloc/loadable_state_bloc.dart';
|
||||
import 'loadable_state_consumer.dart';
|
||||
|
||||
class LoadableStateErrorScreen extends StatelessWidget {
|
||||
final bool visible;
|
||||
final String? message;
|
||||
const LoadableStateErrorScreen({required this.visible, this.message, super.key});
|
||||
|
||||
final String? technicalDetails;
|
||||
const LoadableStateErrorScreen({
|
||||
required this.visible,
|
||||
this.message,
|
||||
this.technicalDetails,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOffline = bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final headline = isOffline ? bloc.connectionText() : (message ?? bloc.connectionText());
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: !visible ? null : Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 10),
|
||||
Text(bloc.connectionText(), style: const TextStyle(fontSize: 20)),
|
||||
|
||||
if(bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 10),
|
||||
TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')),
|
||||
const SizedBox(height: 40),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
message ?? 'Task failed successfully :)',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 12
|
||||
),
|
||||
maxLines: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
headline,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (!isOffline && message != null && message != headline) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => bloc.reFetch!(),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
if (technicalDetails != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => InfoDialog.show(context, technicalDetails!),
|
||||
child: const Text('Details anzeigen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../widget/app_progress_indicator.dart';
|
||||
import 'loadable_state_consumer.dart';
|
||||
|
||||
class LoadableStatePrimaryLoading extends StatelessWidget {
|
||||
@@ -11,6 +12,6 @@ class LoadableStatePrimaryLoading extends StatelessWidget {
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ import 'dart:developer';
|
||||
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../../../../../api/errors/error_mapper.dart';
|
||||
import '../../loadableState/loading_error.dart';
|
||||
import '../../repository/repository.dart';
|
||||
import 'loadable_hydrated_bloc_event.dart';
|
||||
@@ -78,8 +79,9 @@ abstract class LoadableHydratedBloc<
|
||||
(e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
add(Error(LoadingError(
|
||||
message: e.message ?? e.toString(),
|
||||
allowRetry: true,
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
)));
|
||||
},
|
||||
).then((value) {
|
||||
|
||||
Reference in New Issue
Block a user