claude refactorings, flutter best practices, platform dependent changes, general cleanup
This commit is contained in:
@@ -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
|
||||
+21
@@ -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()),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user