claude refactorings, flutter best practices, platform dependent changes, general cleanup

This commit is contained in:
2026-05-06 11:58:50 +02:00
parent 4b1d4379a0
commit 4e1272aba9
281 changed files with 1948 additions and 1041 deletions
@@ -0,0 +1,61 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'loadable_state_event.dart';
import 'loadable_state_state.dart';
class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
late StreamSubscription<List<ConnectivityResult>> _updateStream;
void Function()? reFetch;
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
on<ConnectivityChanged>((event, emit) {
emit(event.state);
if(connectivityStatusKnown() && isConnected()) {
if(reFetch == null) return;
reFetch!();
}
});
void emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
Connectivity().checkConnectivity().then(emitConnectivity);
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
}
bool connectivityStatusKnown() => state.connections != null;
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.device_unknown;
Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
? 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'
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}'
: 'Unbekannte Fehlerursache';
@override
Future<void> close() {
_updateStream.cancel();
return super.close();
}
}
@@ -0,0 +1,7 @@
import 'loadable_state_state.dart';
sealed class LoadableStateEvent {}
final class ConnectivityChanged extends LoadableStateEvent {
final LoadableStateState state;
ConnectivityChanged(this.state);
}
@@ -0,0 +1,11 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'loadable_state_state.freezed.dart';
@freezed
abstract class LoadableStateState with _$LoadableStateState {
const factory LoadableStateState({
required List<ConnectivityResult>? connections,
}) = _LoadableStateState;
}
@@ -0,0 +1,279 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'loadable_state_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LoadableStateState {
List<ConnectivityResult>? get connections;
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LoadableStateStateCopyWith<LoadableStateState> get copyWith => _$LoadableStateStateCopyWithImpl<LoadableStateState>(this as LoadableStateState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableStateState&&const DeepCollectionEquality().equals(other.connections, connections));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(connections));
@override
String toString() {
return 'LoadableStateState(connections: $connections)';
}
}
/// @nodoc
abstract mixin class $LoadableStateStateCopyWith<$Res> {
factory $LoadableStateStateCopyWith(LoadableStateState value, $Res Function(LoadableStateState) _then) = _$LoadableStateStateCopyWithImpl;
@useResult
$Res call({
List<ConnectivityResult>? connections
});
}
/// @nodoc
class _$LoadableStateStateCopyWithImpl<$Res>
implements $LoadableStateStateCopyWith<$Res> {
_$LoadableStateStateCopyWithImpl(this._self, this._then);
final LoadableStateState _self;
final $Res Function(LoadableStateState) _then;
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? connections = freezed,}) {
return _then(_self.copyWith(
connections: freezed == connections ? _self.connections : connections // ignore: cast_nullable_to_non_nullable
as List<ConnectivityResult>?,
));
}
}
/// Adds pattern-matching-related methods to [LoadableStateState].
extension LoadableStateStatePatterns on LoadableStateState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LoadableStateState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LoadableStateState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LoadableStateState value) $default,){
final _that = this;
switch (_that) {
case _LoadableStateState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LoadableStateState value)? $default,){
final _that = this;
switch (_that) {
case _LoadableStateState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<ConnectivityResult>? connections)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LoadableStateState() when $default != null:
return $default(_that.connections);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<ConnectivityResult>? connections) $default,) {final _that = this;
switch (_that) {
case _LoadableStateState():
return $default(_that.connections);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<ConnectivityResult>? connections)? $default,) {final _that = this;
switch (_that) {
case _LoadableStateState() when $default != null:
return $default(_that.connections);case _:
return null;
}
}
}
/// @nodoc
class _LoadableStateState implements LoadableStateState {
const _LoadableStateState({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);
}
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LoadableStateStateCopyWith<_LoadableStateState> get copyWith => __$LoadableStateStateCopyWithImpl<_LoadableStateState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableStateState&&const DeepCollectionEquality().equals(other._connections, _connections));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_connections));
@override
String toString() {
return 'LoadableStateState(connections: $connections)';
}
}
/// @nodoc
abstract mixin class _$LoadableStateStateCopyWith<$Res> implements $LoadableStateStateCopyWith<$Res> {
factory _$LoadableStateStateCopyWith(_LoadableStateState value, $Res Function(_LoadableStateState) _then) = __$LoadableStateStateCopyWithImpl;
@override @useResult
$Res call({
List<ConnectivityResult>? connections
});
}
/// @nodoc
class __$LoadableStateStateCopyWithImpl<$Res>
implements _$LoadableStateStateCopyWith<$Res> {
__$LoadableStateStateCopyWithImpl(this._self, this._then);
final _LoadableStateState _self;
final $Res Function(_LoadableStateState) _then;
/// Create a copy of LoadableStateState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? connections = freezed,}) {
return _then(_LoadableStateState(
connections: freezed == connections ? _self._connections : connections // ignore: cast_nullable_to_non_nullable
as List<ConnectivityResult>?,
));
}
}
// dart format on
@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'loading_error.dart';
part 'loadable_state.freezed.dart';
@freezed
abstract class LoadableState<TState> with _$LoadableState<TState> {
const LoadableState._();
const factory LoadableState({
required bool isLoading,
required TState? data,
required int? lastFetch,
required void Function()? reFetch,
required LoadingError? error,
}) = _LoadableState<TState>;
bool _hasError() => error != null;
bool _hasData() => data != null;
bool showPrimaryLoading() => isLoading && !_hasData();
bool showBackgroundLoading() => isLoading && _hasData();
bool showErrorBar() => _hasError() && _hasData();
bool showError() => _hasError() && !_hasData();
bool showContent() => _hasData();
}
@@ -0,0 +1,307 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'loadable_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LoadableState<TState> {
bool get isLoading; TState? get data; int? get lastFetch; void Function()? get reFetch; LoadingError? get error;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith => _$LoadableStateCopyWithImpl<TState, LoadableState<TState>>(this as LoadableState<TState>, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error);
@override
String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
}
}
/// @nodoc
abstract mixin class $LoadableStateCopyWith<TState,$Res> {
factory $LoadableStateCopyWith(LoadableState<TState> value, $Res Function(LoadableState<TState>) _then) = _$LoadableStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error
});
$LoadingErrorCopyWith<$Res>? get error;
}
/// @nodoc
class _$LoadableStateCopyWithImpl<TState,$Res>
implements $LoadableStateCopyWith<TState, $Res> {
_$LoadableStateCopyWithImpl(this._self, this._then);
final LoadableState<TState> _self;
final $Res Function(LoadableState<TState>) _then;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as TState?,lastFetch: freezed == lastFetch ? _self.lastFetch : lastFetch // ignore: cast_nullable_to_non_nullable
as int?,reFetch: freezed == reFetch ? _self.reFetch : reFetch // ignore: cast_nullable_to_non_nullable
as void Function()?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
));
}
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$LoadingErrorCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $LoadingErrorCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
/// Adds pattern-matching-related methods to [LoadableState].
extension LoadableStatePatterns<TState> on LoadableState<TState> {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LoadableState<TState> value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LoadableState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LoadableState<TState> value) $default,){
final _that = this;
switch (_that) {
case _LoadableState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LoadableState<TState> value)? $default,){
final _that = this;
switch (_that) {
case _LoadableState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LoadableState() when $default != null:
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error) $default,) {final _that = this;
switch (_that) {
case _LoadableState():
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error)? $default,) {final _that = this;
switch (_that) {
case _LoadableState() when $default != null:
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
return null;
}
}
}
/// @nodoc
class _LoadableState<TState> extends LoadableState<TState> {
const _LoadableState({required this.isLoading, required this.data, required this.lastFetch, required this.reFetch, required this.error}): super._();
@override final bool isLoading;
@override final TState? data;
@override final int? lastFetch;
@override final void Function()? reFetch;
@override final LoadingError? error;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LoadableStateCopyWith<TState, _LoadableState<TState>> get copyWith => __$LoadableStateCopyWithImpl<TState, _LoadableState<TState>>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error);
@override
String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
}
}
/// @nodoc
abstract mixin class _$LoadableStateCopyWith<TState,$Res> implements $LoadableStateCopyWith<TState, $Res> {
factory _$LoadableStateCopyWith(_LoadableState<TState> value, $Res Function(_LoadableState<TState>) _then) = __$LoadableStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error
});
@override $LoadingErrorCopyWith<$Res>? get error;
}
/// @nodoc
class __$LoadableStateCopyWithImpl<TState,$Res>
implements _$LoadableStateCopyWith<TState, $Res> {
__$LoadableStateCopyWithImpl(this._self, this._then);
final _LoadableState<TState> _self;
final $Res Function(_LoadableState<TState>) _then;
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,}) {
return _then(_LoadableState<TState>(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as TState?,lastFetch: freezed == lastFetch ? _self.lastFetch : lastFetch // ignore: cast_nullable_to_non_nullable
as int?,reFetch: freezed == reFetch ? _self.reFetch : reFetch // ignore: cast_nullable_to_non_nullable
as void Function()?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as LoadingError?,
));
}
/// Create a copy of LoadableState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$LoadingErrorCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $LoadingErrorCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
// dart format on
@@ -0,0 +1,12 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'loading_error.freezed.dart';
@freezed
abstract class LoadingError with _$LoadingError {
const factory LoadingError({
required String message,
String? technicalDetails,
@Default(false) bool allowRetry,
}) = _LoadingError;
}
@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// 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 'loading_error.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LoadingError {
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)
@pragma('vm:prefer-inline')
$LoadingErrorCopyWith<LoadingError> get copyWith => _$LoadingErrorCopyWithImpl<LoadingError>(this as LoadingError, _$identity);
@override
bool operator ==(Object other) {
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,technicalDetails,allowRetry);
@override
String toString() {
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
}
}
/// @nodoc
abstract mixin class $LoadingErrorCopyWith<$Res> {
factory $LoadingErrorCopyWith(LoadingError value, $Res Function(LoadingError) _then) = _$LoadingErrorCopyWithImpl;
@useResult
$Res call({
String message, String? technicalDetails, bool allowRetry
});
}
/// @nodoc
class _$LoadingErrorCopyWithImpl<$Res>
implements $LoadingErrorCopyWith<$Res> {
_$LoadingErrorCopyWithImpl(this._self, this._then);
final LoadingError _self;
final $Res Function(LoadingError) _then;
/// 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? technicalDetails = freezed,Object? allowRetry = null,}) {
return _then(_self.copyWith(
message: null == message ? _self.message : message // 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,
));
}
}
/// Adds pattern-matching-related methods to [LoadingError].
extension LoadingErrorPatterns on LoadingError {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LoadingError value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LoadingError() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LoadingError value) $default,){
final _that = this;
switch (_that) {
case _LoadingError():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LoadingError value)? $default,){
final _that = this;
switch (_that) {
case _LoadingError() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@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.technicalDetails,_that.allowRetry);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@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.technicalDetails,_that.allowRetry);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@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.technicalDetails,_that.allowRetry);case _:
return null;
}
}
}
/// @nodoc
class _LoadingError implements LoadingError {
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
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LoadingErrorCopyWith<_LoadingError> get copyWith => __$LoadingErrorCopyWithImpl<_LoadingError>(this, _$identity);
@override
bool operator ==(Object other) {
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,technicalDetails,allowRetry);
@override
String toString() {
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
}
}
/// @nodoc
abstract mixin class _$LoadingErrorCopyWith<$Res> implements $LoadingErrorCopyWith<$Res> {
factory _$LoadingErrorCopyWith(_LoadingError value, $Res Function(_LoadingError) _then) = __$LoadingErrorCopyWithImpl;
@override @useResult
$Res call({
String message, String? technicalDetails, bool allowRetry
});
}
/// @nodoc
class __$LoadingErrorCopyWithImpl<$Res>
implements _$LoadingErrorCopyWith<$Res> {
__$LoadingErrorCopyWithImpl(this._self, this._then);
final _LoadingError _self;
final $Res Function(_LoadingError) _then;
/// 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? technicalDetails = freezed,Object? allowRetry = null,}) {
return _then(_LoadingError(
message: null == message ? _self.message : message // 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,
));
}
}
// dart format on
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'loadable_state_consumer.dart';
class LoadableStateBackgroundLoading extends StatelessWidget {
final bool visible;
const LoadableStateBackgroundLoading({required this.visible, super.key});
@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(),
);
}
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/conditional_wrapper.dart';
import '../../utility_widgets/bloc_module.dart';
import '../../utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../bloc/loadable_state_bloc.dart';
import '../bloc/loadable_state_state.dart';
import '../loadable_state.dart';
import 'loadable_state_background_loading.dart';
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 {
final Widget Function(TState state, bool loading) child;
final void Function(TState state)? onLoad;
final bool wrapWithScrollView;
/// Optional predicate for callers whose [TState] always contains a non-null
/// envelope but where actual content (e.g. a nested response) is loaded
/// lazily. When provided, this overrides the default `data != null` check
/// so primary loading / error screens / content visibility correctly reflect
/// whether the inner content is ready.
final bool Function(TState state)? isReady;
const LoadableStateConsumer({
required this.child,
this.onLoad,
this.wrapWithScrollView = false,
this.isReady,
super.key,
});
static Duration animationDuration = const Duration(milliseconds: 200);
@override
Widget build(BuildContext context) {
var loadableState = context.watch<TController>().state;
final loadedData = loadableState.data;
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 hasError = loadableState.error != null;
final isLoading = loadableState.isLoading;
final showPrimaryLoading = isLoading && !hasContent;
final showBackgroundLoading = isLoading && hasContent;
final showError = hasError && !hasContent;
final showErrorBar = hasError && hasContent;
var childWidget = ConditionalWrapper(
condition: loadableState.reFetch != null,
wrapper: (child) => RefreshIndicator(
onRefresh: () {
if(loadableState.reFetch != null) loadableState.reFetch!();
return Future.value();
},
child: ConditionalWrapper(
condition: wrapWithScrollView,
wrapper: (child) => SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: child
),
child: child,
)
),
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: hasContent
? child(typedData as TState, isLoading)
: const SizedBox.shrink(),
),
);
return BlocModule<LoadableStateBloc, LoadableStateState>(
create: (context) => LoadableStateBloc(),
child: (context, bloc, state) {
bloc.reFetch = loadableState.reFetch;
return Column(
children: [
LoadableStateErrorBar(
visible: showErrorBar,
hasContent: hasContent,
message: loadableState.error?.message,
technicalDetails: loadableState.error?.technicalDetails,
lastUpdated: loadableState.lastFetch,
),
Expanded(
child: Stack(
children: [
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
LoadableStateErrorScreen(
visible: showError,
message: loadableState.error?.message,
technicalDetails: loadableState.error?.technicalDetails,
),
AnimatedOpacity(
opacity: hasContent ? 1.0 : 0.0,
duration: animationDuration,
curve: Curves.easeInOut,
child: childWidget,
),
],
),
)
],
);
}
);
}
}
@@ -0,0 +1,115 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/info_dialog.dart';
import '../bloc/loadable_state_bloc.dart';
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,
});
final Duration animationDuration = const Duration(milliseconds: 200);
@override
Widget build(BuildContext context) {
final bloc = context.watch<LoadableStateBloc>();
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);
},
child: Container(
height: 20,
decoration: BoxDecoration(
color: bloc.connectionColor(context),
),
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
),
);
},
)
)
),
);
}
}
class LoadableStateErrorBarText extends StatefulWidget {
final int? lastUpdated;
const LoadableStateErrorBarText({required this.lastUpdated, super.key});
@override
State<LoadableStateErrorBarText> createState() => _LoadableStateErrorBarTextState();
}
class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
late Timer _rebuildTimer;
@override
void initState() {
_rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {}));
super.initState();
}
@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, color: foreground),
const SizedBox(width: 10),
Text(
bloc.connectionText(lastUpdated: widget.lastUpdated),
style: TextStyle(fontSize: 12, color: foreground),
),
],
);
}
@override
void dispose() {
_rebuildTimer.cancel();
super.dispose();
}
}
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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;
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: 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'),
),
],
],
),
),
),
);
}
}
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import '../../../../../widget/app_progress_indicator.dart';
import 'loadable_state_consumer.dart';
class LoadableStatePrimaryLoading extends StatelessWidget {
final bool visible;
const LoadableStatePrimaryLoading({required this.visible, super.key});
@override
Widget build(BuildContext context) => AnimatedOpacity(
opacity: visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: const Center(child: AppProgressIndicator.large()),
);
}