dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
@@ -21,16 +21,19 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
on<ConnectivityChanged>((event, emit) {
emit(event.state);
if(connectivityStatusKnown() && isConnected()) {
if(reFetch == null) return;
if (connectivityStatusKnown() && isConnected()) {
if (reFetch == null) return;
reFetch!();
}
});
void emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
void emitConnectivity(List<ConnectivityResult> result) =>
add(ConnectivityChanged(LoadableStateState(connections: result)));
Connectivity().checkConnectivity().then(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(
emitConnectivity,
);
WidgetsBinding.instance.addObserver(this);
}
@@ -38,42 +41,51 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state != AppLifecycleState.resumed) return;
final now = DateTime.now();
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) return;
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) {
return;
}
_lastResumeRefetch = now;
// Re-check connectivity. The resulting [ConnectivityChanged] event takes
// it from there: its handler updates the offline/online indicator and
// triggers [reFetch] when the device is connected, so a stale
// "Verbindung fehlgeschlagen" bar from a suspend-time fetch clears as
// soon as the network is reachable again.
unawaited(Connectivity().checkConnectivity().then(
(result) => add(ConnectivityChanged(LoadableStateState(connections: result))),
));
unawaited(
Connectivity().checkConnectivity().then(
(result) =>
add(ConnectivityChanged(LoadableStateState(connections: result))),
),
);
}
bool connectivityStatusKnown() => state.connections != null;
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
bool isConnected() =>
!(state.connections?.contains(ConnectivityResult.none) ?? true);
bool allowRetry() => reFetch != null;
IconData connectionIcon() => connectivityStatusKnown()
? isConnected()
? Icons.nearby_error
: Icons.signal_wifi_connected_no_internet_4
? Icons.nearby_error
: Icons.signal_wifi_connected_no_internet_4
: Icons.device_unknown;
Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
Color connectionColor(BuildContext context) =>
connectivityStatusKnown() && !isConnected()
? Colors.grey.shade600
: Theme.of(context).primaryColor;
Color connectionForegroundColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
Color connectionForegroundColor(BuildContext context) =>
connectivityStatusKnown() && !isConnected()
? Colors.white
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == Brightness.dark
? Colors.white
: Colors.black;
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) ==
Brightness.dark
? Colors.white
: Colors.black;
String connectionText({int? lastUpdated}) => connectivityStatusKnown()
? isConnected()
? 'Verbindung fehlgeschlagen'
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
? 'Verbindung fehlgeschlagen'
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
: 'Unbekannte Fehlerursache';
@override
@@ -1,6 +1,7 @@
import 'loadable_state_state.dart';
sealed class LoadableStateEvent {}
final class ConnectivityChanged extends LoadableStateEvent {
final LoadableStateState state;
ConnectivityChanged(this.state);
@@ -8,14 +8,15 @@ class LoadableStateBackgroundLoading extends StatelessWidget {
@override
Widget build(BuildContext context) => AnimatedSwitcher(
duration: LoadableStateConsumer.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(),
);
duration: LoadableStateConsumer.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(),
);
}
@@ -12,7 +12,12 @@ import 'loadable_state_error_bar.dart';
import 'loadable_state_error_screen.dart';
import 'loadable_state_primary_loading.dart';
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
class LoadableStateConsumer<
TController
extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>,
TState
>
extends StatelessWidget {
final Widget Function(TState state, bool loading) child;
final void Function(TState state)? onLoad;
final bool wrapWithScrollView;
@@ -39,12 +44,16 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
var loadableState = context.watch<TController>().state;
final loadedData = loadableState.data;
if(!loadableState.isLoading && onLoad != null && loadedData is TState) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData));
if (!loadableState.isLoading && onLoad != null && loadedData is TState) {
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) => onLoad!(loadedData),
);
}
final typedData = loadedData is TState ? loadedData : null;
final hasContent = typedData != null && (isReady?.call(typedData) ?? loadableState.showContent());
final hasContent =
typedData != null &&
(isReady?.call(typedData) ?? loadableState.showContent());
final hasError = loadableState.error != null;
final isLoading = loadableState.isLoading;
@@ -57,23 +66,23 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
condition: loadableState.reFetch != null,
wrapper: (child) => RefreshIndicator(
onRefresh: () {
if(loadableState.reFetch != null) loadableState.reFetch!();
if (loadableState.reFetch != null) loadableState.reFetch!();
return Future.value();
},
child: ConditionalWrapper(
condition: wrapWithScrollView,
wrapper: (child) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: child
child: child,
),
child: child,
)
),
),
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: hasContent
? child(typedData as TState, isLoading)
: const SizedBox.shrink(),
? child(typedData as TState, isLoading)
: const SizedBox.shrink(),
),
);
@@ -94,7 +103,9 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
child: Stack(
children: [
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
LoadableStateBackgroundLoading(
visible: showBackgroundLoading,
),
LoadableStateErrorScreen(
visible: showError,
message: loadableState.error?.message,
@@ -109,10 +120,10 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
),
],
),
)
),
],
);
}
},
);
}
}
@@ -26,48 +26,57 @@ class LoadableStateErrorBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = context.watch<LoadableStateBloc>();
final isOfflineWithCache = hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
final isOfflineWithCache =
hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
final shouldShow = visible || isOfflineWithCache;
return AnimatedSize(
duration: animationDuration,
child: 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(
key: Key(shouldShow.hashCode.toString()),
visible: shouldShow,
replacement: const SizedBox(width: double.infinity),
child: Builder(
builder: (context) {
var bloc = context.watch<LoadableStateBloc>();
return InkWell(
onTap: () {
if(!bloc.isConnected()) return;
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, copyable: true, title: 'Fehlerdetails');
},
child: Container(
height: 20,
decoration: BoxDecoration(
color: bloc.connectionColor(context),
),
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
),
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(
key: Key(shouldShow.hashCode.toString()),
visible: shouldShow,
replacement: const SizedBox(width: double.infinity),
child: Builder(
builder: (context) {
var bloc = context.watch<LoadableStateBloc>();
return InkWell(
onTap: () {
if (!bloc.isConnected()) return;
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,
copyable: true,
title: 'Fehlerdetails',
);
},
)
)
child: Container(
height: 20,
decoration: BoxDecoration(
color: bloc.connectionColor(context),
),
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
),
);
},
),
),
),
);
}
@@ -78,14 +87,18 @@ class LoadableStateErrorBarText extends StatefulWidget {
const LoadableStateErrorBarText({required this.lastUpdated, super.key});
@override
State<LoadableStateErrorBarText> createState() => _LoadableStateErrorBarTextState();
State<LoadableStateErrorBarText> createState() =>
_LoadableStateErrorBarTextState();
}
class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
late Timer _rebuildTimer;
@override
void initState() {
_rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {}));
_rebuildTimer = Timer.periodic(
const Duration(seconds: 10),
(timer) => setState(() {}),
);
super.initState();
}
@@ -20,52 +20,66 @@ class LoadableStateErrorScreen extends StatelessWidget {
Widget build(BuildContext context) {
final bloc = context.watch<LoadableStateBloc>();
final isOffline = bloc.connectivityStatusKnown() && !bloc.isConnected();
final headline = isOffline ? bloc.connectionText() : (message ?? bloc.connectionText());
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: 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,
child: !visible
? null
: Center(
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!,
copyable: true,
title: 'Fehlerdetails',
),
child: const Text('Details anzeigen'),
),
],
],
),
),
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!, copyable: true, title: 'Fehlerdetails'),
child: const Text('Details anzeigen'),
),
],
],
),
),
),
),
);
}
}
@@ -9,9 +9,9 @@ class LoadableStatePrimaryLoading extends StatelessWidget {
@override
Widget build(BuildContext context) => AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: const Center(child: AppProgressIndicator.large()),
);
opacity: visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: const Center(child: AppProgressIndicator.large()),
);
}