improved performance and battery usage on older devices

This commit is contained in:
2026-09-25 23:58:08 +02:00
parent ed8e52f475
commit 56a497985b
70 changed files with 1631 additions and 621 deletions
@@ -6,6 +6,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import '../../../../../api/errors/error_mapper.dart';
import '../../../../../api/errors/stale_session_exception.dart';
import '../../../../../model/account_data.dart';
import '../../../../../utils/session_single_flight.dart';
import '../../loadable_state/loadable_state.dart';
import '../../loadable_state/loading_error.dart';
import '../../repository/repository.dart';
@@ -20,6 +21,12 @@ abstract class LoadableHydratedBloc<
extends
HydratedBloc<LoadableHydratedBlocEvent<TState>, LoadableState<TState>> {
late TRepository _repository;
// HydratedBloc serialises and writes the full state on every emit; loading
// flags, status texts and errors re-emit the very same data, so those
// writes are skipped (see [persistenceKey]).
Object? _lastPersistedKey;
int? _lastPersistedFetch;
LoadableHydratedBloc()
: super(
const LoadableState(
@@ -113,6 +120,8 @@ abstract class LoadableHydratedBloc<
/// fresh [fetch] (e.g. via [retry] or page-specific refresh) once the user
/// is authenticated again, otherwise the UI would stay blank.
Future<void> reset() async {
_lastPersistedKey = null;
_lastPersistedFetch = null;
await clear();
add(Reset<TState>());
}
@@ -122,10 +131,8 @@ abstract class LoadableHydratedBloc<
/// Runs [body] tagged with the current session: events it adds, also from
/// its async continuations, are dropped once the account signed out, so a
/// late response of the previous account cannot refill the reset bloc.
R runInSession<R>(R Function() body) => runZoned(
body,
zoneValues: {_sessionKey: AccountData().sessionEpoch},
);
R runInSession<R>(R Function() body) =>
runZoned(body, zoneValues: {_sessionKey: AccountData().sessionEpoch});
@override
void add(LoadableHydratedBlocEvent<TState> event) {
@@ -160,20 +167,31 @@ abstract class LoadableHydratedBloc<
);
}
// The constructor, the app shell and resume/reconnect handlers can all ask
// at once; each parallel gather would re-parse, re-emit and re-persist the
// same data.
final SessionSingleFlight _fetchFlight = SessionSingleFlight();
void fetch() {
log('Fetching data for ${TState.toString()}');
runInSession(
() => gatherData()
.catchError((Object e) {
log('Error while fetching ${TState.toString()}: ${e.toString()}');
// The bloc may have been closed before this async error landed;
// adding to a closed bloc throws, so swallow that case.
if (isClosed) return;
addLoadingError(e);
})
.then((value) {
log('Fetch for ${TState.toString()} completed!');
}),
unawaited(
_fetchFlight.run(() {
log('Fetching data for ${TState.toString()}');
return runInSession(
() => gatherData()
.catchError((Object e) {
log(
'Error while fetching ${TState.toString()}: ${e.toString()}',
);
// The bloc may have been closed before this async error
// landed; adding to a closed bloc throws, so swallow that case.
if (isClosed) return;
addLoadingError(e);
})
.then((value) {
log('Fetch for ${TState.toString()} completed!');
}),
);
}),
);
}
@@ -191,13 +209,27 @@ abstract class LoadableHydratedBloc<
@override
Map<String, dynamic>? toJson(LoadableState<TState> state) {
final stateData = state.data;
final key = stateData is TState ? persistenceKey(stateData) : null;
if (key != null &&
// Identity for plain data: a freezed `==` would deep-compare it.
(identical(key, _lastPersistedKey) ||
(key is Record && key == _lastPersistedKey)) &&
state.lastFetch == _lastPersistedFetch) {
return null;
}
_lastPersistedKey = key;
_lastPersistedFetch = state.lastFetch;
Map<String, dynamic>? data;
try {
final stateData = state.data;
data = stateData is TState ? toStorage(stateData) : null;
} catch (e) {
log('Failed to save state ${TState.toString()}: ${e.toString()}');
}
// Blocs that keep nothing on disk return null from toStorage; writing an
// empty wrapper per emit would be pure overhead.
if (stateData != null && data == null) return null;
return LoadableSaveContext.wrap(
data,
@@ -205,6 +237,13 @@ abstract class LoadableHydratedBloc<
);
}
/// What has to change for the state to be written to disk again. Defaults
/// to the data instance; blocs whose state also carries transient UI flags
/// can return a record of just the persisted parts (records compare with
/// `==`, so use fields that compare cheaply, e.g. by identity).
Object? persistenceKey(TState data) => data;
Future<void> gatherData();
TRepository repository();