loading state and error handling refactor

This commit is contained in:
2026-05-06 10:11:45 +02:00
parent 2c376afd91
commit 4b1d4379a0
48 changed files with 1377 additions and 354 deletions
@@ -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()),
);
}