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
@@ -14,10 +14,14 @@ abstract class DataLoader<TResult> {
Future<TResult> run() async {
final response = await fetch();
try {
return assemble(DataLoaderResult(
json: jsonDecode(response.data!),
headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))),
));
return assemble(
DataLoaderResult(
json: jsonDecode(response.data!),
headers: response.headers.map.map(
(key, value) => MapEntry(key, value.join(';')),
),
),
);
} catch (e, stack) {
log('DataLoader assemble failed', error: e, stackTrace: stack);
rethrow;
@@ -34,7 +38,8 @@ class DataLoaderResult {
Map<String, dynamic> asMap() => json as Map<String, dynamic>;
List<dynamic> asList() => json as List<dynamic>;
List<Map<String, dynamic>> asListOfMaps() => asList().map((e) => e as Map<String, dynamic>).toList();
List<Map<String, dynamic>> asListOfMaps() =>
asList().map((e) => e as Map<String, dynamic>).toList();
DataLoaderResult({required this.json, required this.headers});
}
@@ -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()),
);
}
@@ -1,15 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends StatelessWidget {
class BlocModule<TBloc extends StateStreamableSource<TState>, TState>
extends StatelessWidget {
final TBloc Function(BuildContext context) create;
final Widget Function(BuildContext context, TBloc bloc, TState state) child;
final bool autoRebuild;
final void Function(BuildContext context, TBloc bloc)? onInitialisation;
const BlocModule({required this.create, required this.child, this.autoRebuild = false, this.onInitialisation, super.key});
const BlocModule({
required this.create,
required this.child,
this.autoRebuild = false,
this.onInitialisation,
super.key,
});
Widget rebuildChild(BuildContext context) => child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
Widget rebuildChild(BuildContext context) =>
child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
Widget staticChild(BuildContext context) =>
child(context, context.read<TBloc>(), context.read<TBloc>().state);
@override
Widget build(BuildContext context) => BlocProvider<TBloc>(
@@ -19,9 +28,8 @@ class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends St
return bloc;
},
child: Builder(
builder: (context) => autoRebuild
? rebuildChild(context)
: staticChild(context)
)
builder: (context) =>
autoRebuild ? rebuildChild(context) : staticChild(context),
),
);
}
@@ -13,60 +13,79 @@ abstract class LoadableHydratedBloc<
TEvent extends LoadableHydratedBlocEvent<TState>,
TState,
TRepository extends Repository<TState>
> extends HydratedBloc<
LoadableHydratedBlocEvent<TState>,
LoadableState<TState>
> {
>
extends
HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> {
late TRepository _repository;
LoadableHydratedBloc() : super(const LoadableState(
error: null,
data: null,
isLoading: true,
lastFetch: null,
reFetch: null,
)) {
LoadableHydratedBloc()
: super(
const LoadableState(
error: null,
data: null,
isLoading: true,
lastFetch: null,
reFetch: null,
),
) {
on<Emit<TState>>((event, emit) {
emit(LoadableState(
isLoading: state.isLoading,
data: event.state(innerState ?? fromNothing()),
lastFetch: state.lastFetch,
reFetch: retry,
error: state.error,
));
emit(
LoadableState(
isLoading: state.isLoading,
data: event.state(innerState ?? fromNothing()),
lastFetch: state.lastFetch,
reFetch: retry,
error: state.error,
),
);
});
on<DataGathered<TState>>((event, emit) => emit(LoadableState(
isLoading: false,
data: event.state(innerState ?? fromNothing()),
lastFetch: DateTime.now().millisecondsSinceEpoch,
reFetch: retry,
error: null,
)));
on<DataGathered<TState>>(
(event, emit) => emit(
LoadableState(
isLoading: false,
data: event.state(innerState ?? fromNothing()),
lastFetch: DateTime.now().millisecondsSinceEpoch,
reFetch: retry,
error: null,
),
),
);
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
isLoading: true,
data: innerState,
lastFetch: state.lastFetch,
reFetch: null,
error: null,
)));
on<RefetchStarted<TState>>(
(event, emit) => emit(
LoadableState(
isLoading: true,
data: innerState,
lastFetch: state.lastFetch,
reFetch: null,
error: null,
),
),
);
on<Error<TState>>((event, emit) => emit(LoadableState(
isLoading: false,
data: innerState,
lastFetch: state.lastFetch,
reFetch: retry,
error: event.error
)));
on<Error<TState>>(
(event, emit) => emit(
LoadableState(
isLoading: false,
data: innerState,
lastFetch: state.lastFetch,
reFetch: retry,
error: event.error,
),
),
);
on<Reset<TState>>((event, emit) => emit(const LoadableState(
isLoading: false,
data: null,
lastFetch: null,
reFetch: null,
error: null,
)));
on<Reset<TState>>(
(event, emit) => emit(
const LoadableState(
isLoading: false,
data: null,
lastFetch: null,
reFetch: null,
error: null,
),
),
);
_repository = repository();
fetch();
@@ -92,23 +111,27 @@ abstract class LoadableHydratedBloc<
void fetch() {
log('Fetching data for ${TState.toString()}');
gatherData().catchError(
(e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}');
// The bloc may have been closed before this async error landed (e.g.
// when its scoping widget tree was disposed mid-fetch). Adding to a
// closed bloc throws "Cannot add new events after calling close",
// so swallow that case quietly.
if (isClosed) return;
add(Error(LoadingError(
message: errorToUserMessage(e),
technicalDetails: errorToTechnicalDetails(e),
allowRetry: errorAllowsRetry(e),
)));
},
).then((value) {
log('Fetch for ${TState.toString()} completed!');
});
gatherData()
.catchError((e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}');
// The bloc may have been closed before this async error landed (e.g.
// when its scoping widget tree was disposed mid-fetch). Adding to a
// closed bloc throws "Cannot add new events after calling close",
// so swallow that case quietly.
if (isClosed) return;
add(
Error(
LoadingError(
message: errorToUserMessage(e),
technicalDetails: errorToTechnicalDetails(e),
allowRetry: errorAllowsRetry(e),
),
),
);
})
.then((value) {
log('Fetch for ${TState.toString()} completed!');
});
}
@override
@@ -129,13 +152,13 @@ abstract class LoadableHydratedBloc<
try {
final stateData = state.data;
data = stateData is TState ? toStorage(stateData) : null;
} catch(e) {
} catch (e) {
log('Failed to save state ${TState.toString()}: ${e.toString()}');
}
return LoadableSaveContext.wrap(
data,
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch,
);
}
@@ -1,17 +1,22 @@
import '../../loadable_state/loading_error.dart';
class LoadableHydratedBlocEvent<TState> {}
class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
final TState Function(TState state) state;
Emit(this.state);
}
class DataGathered<TState> extends LoadableHydratedBlocEvent<TState> {
final TState Function(TState state) state;
DataGathered(this.state);
}
class Error<TState> extends LoadableHydratedBlocEvent<TState> {
final LoadingError error;
Error(this.error);
}
class RefetchStarted<TState> extends LoadableHydratedBlocEvent<TState> {}
class Reset<TState> extends LoadableHydratedBlocEvent<TState> {}
@@ -6,18 +6,25 @@ part 'loadable_save_context.g.dart';
@freezed
abstract class LoadableSaveContext with _$LoadableSaveContext {
const LoadableSaveContext._();
const factory LoadableSaveContext({
required int timestamp,
}) = _LoadableSaveContext;
const factory LoadableSaveContext({required int timestamp}) =
_LoadableSaveContext;
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) => _$LoadableSaveContextFromJson(json);
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) =>
_$LoadableSaveContextFromJson(json);
static String dataKey = 'data';
static String metaKey = 'meta';
static Map<String, dynamic> wrap(Map<String, dynamic>? data, int lastFetch) =>
{dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()};
{
dataKey: data,
metaKey: LoadableSaveContext(timestamp: lastFetch).toJson(),
};
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>));
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(
Map<String, dynamic> data,
) => (
data: data[dataKey] as Map<String, dynamic>,
meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>),
);
}