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
+51 -20
View File
@@ -10,6 +10,7 @@ import 'api/marianumconnect/marianumconnect_api.dart';
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
import 'main.dart';
import 'model/account_data.dart';
import 'model/data_cleaner.dart';
import 'notification/notification_controller.dart';
import 'notification/notification_tasks.dart';
@@ -40,7 +41,6 @@ class App extends StatefulWidget {
}
class _AppState extends State<App> with WidgetsBindingObserver {
late Timer _updateTimings;
StreamSubscription<dynamic>? _timetableWidgetSync;
StreamSubscription<RemoteMessage>? _onMessageSub;
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
@@ -74,7 +74,11 @@ class _AppState extends State<App> with WidgetsBindingObserver {
bloc.setAutoRefreshInterval(
talkIsActive ? _chatListActiveInterval : _chatListIdleInterval,
);
if (talkIsActive && refresh) bloc.refresh();
// The poll keeps the list current; switching back to the tab shortly
// after a fetch doesn't need another one mid tab transition.
if (talkIsActive && refresh) {
bloc.refreshIfOlderThan(_chatListActiveInterval);
}
}
// Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick
@@ -110,6 +114,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
_syncChatListPolling(refresh: false);
_handlePendingWidgetNavigation();
} else if (mounted) {
context.read<SettingsCubit>().flushSilentSave();
// Stop polling while backgrounded: a silent refresh failing in the
// background would otherwise leave an error that flashes on the next
// resume before the foreground refetch replaces it.
@@ -179,26 +184,42 @@ class _AppState extends State<App> with WidgetsBindingObserver {
final settingsCubit = context.read<SettingsCubit>();
final capabilitiesCubit = context.read<CapabilitiesCubit>();
_timetableWidgetSync?.cancel();
// Debounced: a refresh or week swipe emits several times in a row, and
// only the settled state is worth mirroring.
_timetableWidgetSync = timetable.stream.listen((state) {
final data = state.data;
if (data is TimetableState && !state.isLoading) {
unawaited(
WidgetPublisher.publishFromBlocState(
data,
settings: settingsCubit.val(),
isTeacher: capabilitiesCubit.isTeacher,
final epoch = AccountData().sessionEpoch;
Debouncer.debounce(
'widgetPublish',
const Duration(seconds: 1),
() => unawaited(
WidgetPublisher.publishFromBlocState(
data,
settings: settingsCubit.val(),
isTeacher: capabilitiesCubit.isTeacher,
epoch: epoch,
),
),
);
}
});
// Initial publish in case hydrated storage already has data.
// Initial publish in case hydrated storage already has data. The widget
// still shows its last snapshot, so this waits until the cold-start
// frames are done (a fresh bloc emit in the meantime supersedes it).
final initialData = timetable.state.data;
if (initialData is TimetableState) {
unawaited(
WidgetPublisher.publishFromBlocState(
initialData,
settings: settingsCubit.val(),
isTeacher: capabilitiesCubit.isTeacher,
final epoch = AccountData().sessionEpoch;
Debouncer.debounce(
'widgetPublish',
const Duration(seconds: 3),
() => unawaited(
WidgetPublisher.publishFromBlocState(
initialData,
settings: settingsCubit.val(),
isTeacher: capabilitiesCubit.isTeacher,
epoch: epoch,
),
),
);
}
@@ -208,10 +229,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
_syncChatListPolling();
});
_updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
if (mounted) setState(() {});
});
_reportTelemetry();
// A refreshed FCM token invalidates the existing push subscription — the
@@ -250,12 +267,15 @@ class _AppState extends State<App> with WidgetsBindingObserver {
NotificationController.onAppOpenedByNotification(message, context);
});
DataCleaner.cleanOldCache();
// Housekeeping only; kept out of the cold-start window.
Future<void>.delayed(
const Duration(seconds: 20),
DataCleaner.cleanOldCache,
);
}
@override
void dispose() {
_updateTimings.cancel();
_timetableWidgetSync?.cancel();
_onMessageSub?.cancel();
_onMessageOpenedAppSub?.cancel();
@@ -271,7 +291,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override
Widget build(
BuildContext context,
) => BlocBuilder<SettingsCubit, model.Settings>(
) => BlocSelector<SettingsCubit, model.Settings, Object>(
// Only the module layout shapes the shell; copied values because the
// settings object is mutated in place.
selector: (settings) {
final m = settings.modulesSettings;
return (
m.moduleOrder.join(','),
m.hiddenModules.join(','),
m.autoFillBottomBar,
m.fixedBottomBarSlots,
);
},
builder: (context, _) {
final bottomBarModules = AppModule.getBottomBarModules(context);
final totalTabs = bottomBarModules.length + 1;