dart format
This commit is contained in:
@@ -14,10 +14,14 @@ abstract class DataLoader<TResult> {
|
||||
Future<TResult> run() async {
|
||||
final response = await fetch();
|
||||
try {
|
||||
return assemble(DataLoaderResult(
|
||||
json: jsonDecode(response.data!),
|
||||
headers: response.headers.map.map((key, value) => MapEntry(key, value.join(';'))),
|
||||
));
|
||||
return assemble(
|
||||
DataLoaderResult(
|
||||
json: jsonDecode(response.data!),
|
||||
headers: response.headers.map.map(
|
||||
(key, value) => MapEntry(key, value.join(';')),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log('DataLoader assemble failed', error: e, stackTrace: stack);
|
||||
rethrow;
|
||||
@@ -34,7 +38,8 @@ class DataLoaderResult {
|
||||
|
||||
Map<String, dynamic> asMap() => json as Map<String, dynamic>;
|
||||
List<dynamic> asList() => json as List<dynamic>;
|
||||
List<Map<String, dynamic>> asListOfMaps() => asList().map((e) => e as Map<String, dynamic>).toList();
|
||||
List<Map<String, dynamic>> asListOfMaps() =>
|
||||
asList().map((e) => e as Map<String, dynamic>).toList();
|
||||
|
||||
DataLoaderResult({required this.json, required this.headers});
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
LoadableStateBloc() : super(const LoadableStateState(connections: null)) {
|
||||
on<ConnectivityChanged>((event, emit) {
|
||||
emit(event.state);
|
||||
if(connectivityStatusKnown() && isConnected()) {
|
||||
if(reFetch == null) return;
|
||||
if (connectivityStatusKnown() && isConnected()) {
|
||||
if (reFetch == null) return;
|
||||
reFetch!();
|
||||
}
|
||||
});
|
||||
|
||||
void emitConnectivity(List<ConnectivityResult> result) => add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
void emitConnectivity(List<ConnectivityResult> result) =>
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result)));
|
||||
|
||||
Connectivity().checkConnectivity().then(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(emitConnectivity);
|
||||
_updateStream = Connectivity().onConnectivityChanged.listen(
|
||||
emitConnectivity,
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@@ -38,42 +41,51 @@ class LoadableStateBloc extends Bloc<LoadableStateEvent, LoadableStateState>
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) return;
|
||||
if (now.difference(_lastResumeRefetch) < const Duration(seconds: 10)) {
|
||||
return;
|
||||
}
|
||||
_lastResumeRefetch = now;
|
||||
// Re-check connectivity. The resulting [ConnectivityChanged] event takes
|
||||
// it from there: its handler updates the offline/online indicator and
|
||||
// triggers [reFetch] when the device is connected, so a stale
|
||||
// "Verbindung fehlgeschlagen" bar from a suspend-time fetch clears as
|
||||
// soon as the network is reachable again.
|
||||
unawaited(Connectivity().checkConnectivity().then(
|
||||
(result) => add(ConnectivityChanged(LoadableStateState(connections: result))),
|
||||
));
|
||||
unawaited(
|
||||
Connectivity().checkConnectivity().then(
|
||||
(result) =>
|
||||
add(ConnectivityChanged(LoadableStateState(connections: result))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool connectivityStatusKnown() => state.connections != null;
|
||||
bool isConnected() => !(state.connections?.contains(ConnectivityResult.none) ?? true);
|
||||
bool isConnected() =>
|
||||
!(state.connections?.contains(ConnectivityResult.none) ?? true);
|
||||
bool allowRetry() => reFetch != null;
|
||||
|
||||
IconData connectionIcon() => connectivityStatusKnown()
|
||||
? isConnected()
|
||||
? Icons.nearby_error
|
||||
: Icons.signal_wifi_connected_no_internet_4
|
||||
? Icons.nearby_error
|
||||
: Icons.signal_wifi_connected_no_internet_4
|
||||
: Icons.device_unknown;
|
||||
|
||||
Color connectionColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
|
||||
Color connectionColor(BuildContext context) =>
|
||||
connectivityStatusKnown() && !isConnected()
|
||||
? Colors.grey.shade600
|
||||
: Theme.of(context).primaryColor;
|
||||
|
||||
Color connectionForegroundColor(BuildContext context) => connectivityStatusKnown() && !isConnected()
|
||||
Color connectionForegroundColor(BuildContext context) =>
|
||||
connectivityStatusKnown() && !isConnected()
|
||||
? Colors.white
|
||||
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
: ThemeData.estimateBrightnessForColor(Theme.of(context).primaryColor) ==
|
||||
Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
|
||||
String connectionText({int? lastUpdated}) => connectivityStatusKnown()
|
||||
? isConnected()
|
||||
? 'Verbindung fehlgeschlagen'
|
||||
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
|
||||
? 'Verbindung fehlgeschlagen'
|
||||
: 'Offline${lastUpdated == null ? '' : ' - Stand von ${DateTime.fromMillisecondsSinceEpoch(lastUpdated).formatRelative()}'}'
|
||||
: 'Unbekannte Fehlerursache';
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'loadable_state_state.dart';
|
||||
|
||||
sealed class LoadableStateEvent {}
|
||||
|
||||
final class ConnectivityChanged extends LoadableStateEvent {
|
||||
final LoadableStateState state;
|
||||
ConnectivityChanged(this.state);
|
||||
|
||||
+11
-10
@@ -8,14 +8,15 @@ class LoadableStateBackgroundLoading extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedSwitcher(
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
|
||||
);
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: visible ? const LinearProgressIndicator() : const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import 'loadable_state_error_bar.dart';
|
||||
import 'loadable_state_error_screen.dart';
|
||||
import 'loadable_state_primary_loading.dart';
|
||||
|
||||
class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>, TState> extends StatelessWidget {
|
||||
class LoadableStateConsumer<
|
||||
TController
|
||||
extends Bloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>>,
|
||||
TState
|
||||
>
|
||||
extends StatelessWidget {
|
||||
final Widget Function(TState state, bool loading) child;
|
||||
final void Function(TState state)? onLoad;
|
||||
final bool wrapWithScrollView;
|
||||
@@ -39,12 +44,16 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
var loadableState = context.watch<TController>().state;
|
||||
|
||||
final loadedData = loadableState.data;
|
||||
if(!loadableState.isLoading && onLoad != null && loadedData is TState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData));
|
||||
if (!loadableState.isLoading && onLoad != null && loadedData is TState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(timeStamp) => onLoad!(loadedData),
|
||||
);
|
||||
}
|
||||
|
||||
final typedData = loadedData is TState ? loadedData : null;
|
||||
final hasContent = typedData != null && (isReady?.call(typedData) ?? loadableState.showContent());
|
||||
final hasContent =
|
||||
typedData != null &&
|
||||
(isReady?.call(typedData) ?? loadableState.showContent());
|
||||
final hasError = loadableState.error != null;
|
||||
final isLoading = loadableState.isLoading;
|
||||
|
||||
@@ -57,23 +66,23 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
condition: loadableState.reFetch != null,
|
||||
wrapper: (child) => RefreshIndicator(
|
||||
onRefresh: () {
|
||||
if(loadableState.reFetch != null) loadableState.reFetch!();
|
||||
if (loadableState.reFetch != null) loadableState.reFetch!();
|
||||
return Future.value();
|
||||
},
|
||||
child: ConditionalWrapper(
|
||||
condition: wrapWithScrollView,
|
||||
wrapper: (child) => SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: child
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: hasContent
|
||||
? child(typedData as TState, isLoading)
|
||||
: const SizedBox.shrink(),
|
||||
? child(typedData as TState, isLoading)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -94,7 +103,9 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
child: Stack(
|
||||
children: [
|
||||
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
|
||||
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
|
||||
LoadableStateBackgroundLoading(
|
||||
visible: showBackgroundLoading,
|
||||
),
|
||||
LoadableStateErrorScreen(
|
||||
visible: showError,
|
||||
message: loadableState.error?.message,
|
||||
@@ -109,10 +120,10 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,48 +26,57 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOfflineWithCache = hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final isOfflineWithCache =
|
||||
hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final shouldShow = visible || isOfflineWithCache;
|
||||
|
||||
return AnimatedSize(
|
||||
duration: animationDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) => SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: Visibility(
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if(!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null && technicalDetails!.isNotEmpty) technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(context, body, copyable: true, title: 'Fehlerdetails');
|
||||
},
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: bloc.connectionColor(context),
|
||||
),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
duration: animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
child: Visibility(
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null &&
|
||||
technicalDetails!.isNotEmpty)
|
||||
technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(
|
||||
context,
|
||||
body,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
);
|
||||
},
|
||||
)
|
||||
)
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: bloc.connectionColor(context),
|
||||
),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -78,14 +87,18 @@ class LoadableStateErrorBarText extends StatefulWidget {
|
||||
const LoadableStateErrorBarText({required this.lastUpdated, super.key});
|
||||
|
||||
@override
|
||||
State<LoadableStateErrorBarText> createState() => _LoadableStateErrorBarTextState();
|
||||
State<LoadableStateErrorBarText> createState() =>
|
||||
_LoadableStateErrorBarTextState();
|
||||
}
|
||||
|
||||
class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
|
||||
late Timer _rebuildTimer;
|
||||
@override
|
||||
void initState() {
|
||||
_rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) => setState(() {}));
|
||||
_rebuildTimer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(timer) => setState(() {}),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,52 +20,66 @@ class LoadableStateErrorScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOffline = bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final headline = isOffline ? bloc.connectionText() : (message ?? bloc.connectionText());
|
||||
final headline = isOffline
|
||||
? bloc.connectionText()
|
||||
: (message ?? bloc.connectionText());
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: !visible ? null : Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
headline,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
child: !visible
|
||||
? null
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
headline,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (!isOffline &&
|
||||
message != null &&
|
||||
message != headline) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => bloc.reFetch!(),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
if (technicalDetails != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => InfoDialog.show(
|
||||
context,
|
||||
technicalDetails!,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
),
|
||||
child: const Text('Details anzeigen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isOffline && message != null && message != headline) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message!,
|
||||
style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (bloc.allowRetry()) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => bloc.reFetch!(),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
],
|
||||
if (technicalDetails != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () => InfoDialog.show(context, technicalDetails!, copyable: true, title: 'Fehlerdetails'),
|
||||
child: const Text('Details anzeigen'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,9 +9,9 @@ class LoadableStatePrimaryLoading extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends StatelessWidget {
|
||||
class BlocModule<TBloc extends StateStreamableSource<TState>, TState>
|
||||
extends StatelessWidget {
|
||||
final TBloc Function(BuildContext context) create;
|
||||
final Widget Function(BuildContext context, TBloc bloc, TState state) child;
|
||||
final bool autoRebuild;
|
||||
final void Function(BuildContext context, TBloc bloc)? onInitialisation;
|
||||
const BlocModule({required this.create, required this.child, this.autoRebuild = false, this.onInitialisation, super.key});
|
||||
const BlocModule({
|
||||
required this.create,
|
||||
required this.child,
|
||||
this.autoRebuild = false,
|
||||
this.onInitialisation,
|
||||
super.key,
|
||||
});
|
||||
|
||||
Widget rebuildChild(BuildContext context) => child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
|
||||
Widget staticChild(BuildContext context) => child(context, context.read<TBloc>(), context.read<TBloc>().state);
|
||||
Widget rebuildChild(BuildContext context) =>
|
||||
child(context, context.watch<TBloc>(), context.watch<TBloc>().state);
|
||||
Widget staticChild(BuildContext context) =>
|
||||
child(context, context.read<TBloc>(), context.read<TBloc>().state);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocProvider<TBloc>(
|
||||
@@ -19,9 +28,8 @@ class BlocModule<TBloc extends StateStreamableSource<TState>, TState> extends St
|
||||
return bloc;
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) => autoRebuild
|
||||
? rebuildChild(context)
|
||||
: staticChild(context)
|
||||
)
|
||||
builder: (context) =>
|
||||
autoRebuild ? rebuildChild(context) : staticChild(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+89
-66
@@ -13,60 +13,79 @@ abstract class LoadableHydratedBloc<
|
||||
TEvent extends LoadableHydratedBlocEvent<TState>,
|
||||
TState,
|
||||
TRepository extends Repository<TState>
|
||||
> extends HydratedBloc<
|
||||
LoadableHydratedBlocEvent<TState>,
|
||||
LoadableState<TState>
|
||||
> {
|
||||
>
|
||||
extends
|
||||
HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> {
|
||||
late TRepository _repository;
|
||||
LoadableHydratedBloc() : super(const LoadableState(
|
||||
error: null,
|
||||
data: null,
|
||||
isLoading: true,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
)) {
|
||||
|
||||
LoadableHydratedBloc()
|
||||
: super(
|
||||
const LoadableState(
|
||||
error: null,
|
||||
data: null,
|
||||
isLoading: true,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
),
|
||||
) {
|
||||
on<Emit<TState>>((event, emit) {
|
||||
emit(LoadableState(
|
||||
isLoading: state.isLoading,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: state.error,
|
||||
));
|
||||
emit(
|
||||
LoadableState(
|
||||
isLoading: state.isLoading,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: state.error,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<DataGathered<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: false,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: DateTime.now().millisecondsSinceEpoch,
|
||||
reFetch: retry,
|
||||
error: null,
|
||||
)));
|
||||
on<DataGathered<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: false,
|
||||
data: event.state(innerState ?? fromNothing()),
|
||||
lastFetch: DateTime.now().millisecondsSinceEpoch,
|
||||
reFetch: retry,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<RefetchStarted<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: true,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
)));
|
||||
on<RefetchStarted<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: true,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<Error<TState>>((event, emit) => emit(LoadableState(
|
||||
isLoading: false,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: event.error
|
||||
)));
|
||||
on<Error<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: false,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: event.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<Reset<TState>>((event, emit) => emit(const LoadableState(
|
||||
isLoading: false,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
)));
|
||||
on<Reset<TState>>(
|
||||
(event, emit) => emit(
|
||||
const LoadableState(
|
||||
isLoading: false,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_repository = repository();
|
||||
fetch();
|
||||
@@ -92,23 +111,27 @@ abstract class LoadableHydratedBloc<
|
||||
|
||||
void fetch() {
|
||||
log('Fetching data for ${TState.toString()}');
|
||||
gatherData().catchError(
|
||||
(e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed (e.g.
|
||||
// when its scoping widget tree was disposed mid-fetch). Adding to a
|
||||
// closed bloc throws "Cannot add new events after calling close",
|
||||
// so swallow that case quietly.
|
||||
if (isClosed) return;
|
||||
add(Error(LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
)));
|
||||
},
|
||||
).then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
});
|
||||
gatherData()
|
||||
.catchError((e) {
|
||||
log('Error while fetching ${TState.toString()}: ${e.toString()}');
|
||||
// The bloc may have been closed before this async error landed (e.g.
|
||||
// when its scoping widget tree was disposed mid-fetch). Adding to a
|
||||
// closed bloc throws "Cannot add new events after calling close",
|
||||
// so swallow that case quietly.
|
||||
if (isClosed) return;
|
||||
add(
|
||||
Error(
|
||||
LoadingError(
|
||||
message: errorToUserMessage(e),
|
||||
technicalDetails: errorToTechnicalDetails(e),
|
||||
allowRetry: errorAllowsRetry(e),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.then((value) {
|
||||
log('Fetch for ${TState.toString()} completed!');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -129,13 +152,13 @@ abstract class LoadableHydratedBloc<
|
||||
try {
|
||||
final stateData = state.data;
|
||||
data = stateData is TState ? toStorage(stateData) : null;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
log('Failed to save state ${TState.toString()}: ${e.toString()}');
|
||||
}
|
||||
|
||||
return LoadableSaveContext.wrap(
|
||||
data,
|
||||
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch
|
||||
state.lastFetch ?? DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -1,17 +1,22 @@
|
||||
import '../../loadable_state/loading_error.dart';
|
||||
|
||||
class LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
class Emit<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final TState Function(TState state) state;
|
||||
Emit(this.state);
|
||||
}
|
||||
|
||||
class DataGathered<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final TState Function(TState state) state;
|
||||
DataGathered(this.state);
|
||||
}
|
||||
|
||||
class Error<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final LoadingError error;
|
||||
Error(this.error);
|
||||
}
|
||||
|
||||
class RefetchStarted<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
class Reset<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
+14
-7
@@ -6,18 +6,25 @@ part 'loadable_save_context.g.dart';
|
||||
@freezed
|
||||
abstract class LoadableSaveContext with _$LoadableSaveContext {
|
||||
const LoadableSaveContext._();
|
||||
const factory LoadableSaveContext({
|
||||
required int timestamp,
|
||||
}) = _LoadableSaveContext;
|
||||
const factory LoadableSaveContext({required int timestamp}) =
|
||||
_LoadableSaveContext;
|
||||
|
||||
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) => _$LoadableSaveContextFromJson(json);
|
||||
factory LoadableSaveContext.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoadableSaveContextFromJson(json);
|
||||
|
||||
static String dataKey = 'data';
|
||||
static String metaKey = 'meta';
|
||||
|
||||
static Map<String, dynamic> wrap(Map<String, dynamic>? data, int lastFetch) =>
|
||||
{dataKey: data, metaKey: LoadableSaveContext(timestamp: lastFetch).toJson()};
|
||||
{
|
||||
dataKey: data,
|
||||
metaKey: LoadableSaveContext(timestamp: lastFetch).toJson(),
|
||||
};
|
||||
|
||||
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(Map<String, dynamic> data) =>
|
||||
(data: data[dataKey] as Map<String, dynamic>, meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>));
|
||||
static ({Map<String, dynamic> data, LoadableSaveContext meta}) unwrap(
|
||||
Map<String, dynamic> data,
|
||||
) => (
|
||||
data: data[dataKey] as Map<String, dynamic>,
|
||||
meta: LoadableSaveContext.fromJson(data[metaKey] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user