added timestamp to bloc cache, showing age in offline mode

This commit is contained in:
2024-05-12 02:39:35 +02:00
parent 3281b134e0
commit ebbb70dc96
13 changed files with 302 additions and 40 deletions

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'loadable_state_event.dart';
import 'loadable_state_state.dart';
@ -29,7 +30,6 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
bool connectivityStatusKnown() => state.connections != null;
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
bool allowRetry() => reFetch != null;
bool showErrorMessage() => isConnected() && reFetch != null;
IconData connectionIcon() => connectivityStatusKnown()
? isConnected()
@ -37,10 +37,10 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState> {
: Icons.signal_wifi_connected_no_internet_4
: Icons.device_unknown;
String connectionText() => connectivityStatusKnown()
String connectionText({int? lastUpdated}) => connectivityStatusKnown()
? isConnected()
? 'Verbindung fehlgeschlagen'
: 'Offline'
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${Jiffy.parseFromMillisecondsSinceEpoch(lastUpdated).fromNow()}'}'
: 'Unbekannte Fehlerursache';
@override

View File

@ -11,6 +11,7 @@ class LoadableState<TState> with _$LoadableState {
const factory LoadableState({
@Default(true) bool isLoading,
@Default(null) TState? data,
@Default(null) int? lastFetch,
@Default(null) void Function()? reFetch,
@Default(null) LoadingError? error,
}) = _LoadableState;

View File

@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$LoadableState<TState> {
bool get isLoading => throw _privateConstructorUsedError;
TState? get data => throw _privateConstructorUsedError;
int? get lastFetch => throw _privateConstructorUsedError;
void Function()? get reFetch => throw _privateConstructorUsedError;
LoadingError? get error => throw _privateConstructorUsedError;
@ -35,6 +36,7 @@ abstract class $LoadableStateCopyWith<TState, $Res> {
$Res call(
{bool isLoading,
TState? data,
int? lastFetch,
void Function()? reFetch,
LoadingError? error});
@ -57,6 +59,7 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
$Res call({
Object? isLoading = null,
Object? data = freezed,
Object? lastFetch = freezed,
Object? reFetch = freezed,
Object? error = freezed,
}) {
@ -69,6 +72,10 @@ class _$LoadableStateCopyWithImpl<TState, $Res,
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as TState?,
lastFetch: freezed == lastFetch
? _value.lastFetch
: lastFetch // ignore: cast_nullable_to_non_nullable
as int?,
reFetch: freezed == reFetch
? _value.reFetch
: reFetch // ignore: cast_nullable_to_non_nullable
@ -104,6 +111,7 @@ abstract class _$$LoadableStateImplCopyWith<TState, $Res>
$Res call(
{bool isLoading,
TState? data,
int? lastFetch,
void Function()? reFetch,
LoadingError? error});
@ -125,6 +133,7 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
$Res call({
Object? isLoading = null,
Object? data = freezed,
Object? lastFetch = freezed,
Object? reFetch = freezed,
Object? error = freezed,
}) {
@ -137,6 +146,10 @@ class __$$LoadableStateImplCopyWithImpl<TState, $Res>
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as TState?,
lastFetch: freezed == lastFetch
? _value.lastFetch
: lastFetch // ignore: cast_nullable_to_non_nullable
as int?,
reFetch: freezed == reFetch
? _value.reFetch
: reFetch // ignore: cast_nullable_to_non_nullable
@ -155,6 +168,7 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
const _$LoadableStateImpl(
{this.isLoading = true,
this.data = null,
this.lastFetch = null,
this.reFetch = null,
this.error = null})
: super._();
@ -167,6 +181,9 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
final TState? data;
@override
@JsonKey()
final int? lastFetch;
@override
@JsonKey()
final void Function()? reFetch;
@override
@JsonKey()
@ -174,7 +191,7 @@ class _$LoadableStateImpl<TState> extends _LoadableState<TState> {
@override
String toString() {
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, reFetch: $reFetch, error: $error)';
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
}
@override
@ -185,13 +202,15 @@ class _$LoadableStateImpl<TState> extends _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), reFetch, error);
const DeepCollectionEquality().hash(data), lastFetch, reFetch, error);
@JsonKey(ignore: true)
@override
@ -205,6 +224,7 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
const factory _LoadableState(
{final bool isLoading,
final TState? data,
final int? lastFetch,
final void Function()? reFetch,
final LoadingError? error}) = _$LoadableStateImpl<TState>;
const _LoadableState._() : super._();
@ -214,6 +234,8 @@ abstract class _LoadableState<TState> extends LoadableState<TState> {
@override
TState? get data;
@override
int? get lastFetch;
@override
void Function()? get reFetch;
@override
LoadingError? get error;

View File

@ -53,13 +53,13 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
bloc.reFetch = loadableState.reFetch;
return Column(
children: [
LoadableStateErrorBar(visible: loadableState.showErrorBar()),
LoadableStateErrorBar(visible: loadableState.showErrorBar(), message: loadableState.error?.message, lastUpdated: loadableState.lastFetch),
Expanded(
child: Stack(
children: [
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
LoadableStateErrorScreen(visible: loadableState.showError()),
LoadableStateErrorScreen(visible: loadableState.showError(), message: loadableState.error?.message),
AnimatedOpacity(
opacity: loadableState.showContent() ? 1.0 : 0.0,

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../widget/infoDialog.dart';
import '../bloc/loadable_state_bloc.dart';
class LoadableStateErrorBar extends StatelessWidget {
final bool visible;
const LoadableStateErrorBar({required this.visible, super.key});
final String? message;
final int? lastUpdated;
const LoadableStateErrorBar({required this.visible, this.message, this.lastUpdated, super.key});
final Duration animationDuration = const Duration(milliseconds: 200);
@ -29,25 +32,31 @@ class LoadableStateErrorBar extends StatelessWidget {
builder: (context) {
var bloc = context.watch<LoadableStateBloc>();
var status = (
icon: bloc.connectionIcon(),
text: bloc.connectionText(),
color: bloc.connectivityStatusKnown() && !bloc.isConnected()
icon: bloc.connectionIcon(),
text: bloc.connectionText(lastUpdated: lastUpdated),
color: bloc.connectivityStatusKnown() && !bloc.isConnected()
? Colors.grey.shade600
: Theme.of(context).primaryColor
);
return Container(
height: 20,
decoration: BoxDecoration(
color: status.color,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(status.icon, size: 14),
const SizedBox(width: 10),
Text(status.text, style: const TextStyle(fontSize: 12))
],
return InkWell(
onTap: () {
if(!bloc.isConnected()) return;
InfoDialog.show(context, 'Exception: ${message.toString()}');
},
child: Container(
height: 20,
decoration: BoxDecoration(
color: status.color,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(status.icon, size: 14),
const SizedBox(width: 10),
Text(status.text, style: const TextStyle(fontSize: 12))
],
),
),
);
},

View File

@ -6,7 +6,8 @@ import 'loadable_state_consumer.dart';
class LoadableStateErrorScreen extends StatelessWidget {
final bool visible;
const LoadableStateErrorScreen({required this.visible, super.key});
final String? message;
const LoadableStateErrorScreen({required this.visible, this.message, super.key});
@override
@ -28,11 +29,19 @@ class LoadableStateErrorScreen extends StatelessWidget {
if(bloc.allowRetry()) ...[
const SizedBox(height: 10),
TextButton(onPressed: () => bloc.reFetch!(), child: const Text('Erneut versuschen')),
],
if(bloc.showErrorMessage()) ...[
const SizedBox(height: 40),
Text("bloc.loadingError!.message", style: TextStyle(color: Theme.of(context).hintColor, fontSize: 12))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
message ?? 'Task failed successfully :)',
style: TextStyle(
color: Theme.of(context).hintColor,
fontSize: 12
),
maxLines: 10,
overflow: TextOverflow.ellipsis,
),
),
],
],
),