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
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter_app_badge/flutter_app_badge.dart';
@@ -6,6 +7,7 @@ import 'package:flutter_app_badge/flutter_app_badge.dart';
import '../../../../../api/marianumcloud/talk/actions/talk_actions.dart';
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../../utils/session_single_flight.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../repository/chat_list_repository.dart';
@@ -74,13 +76,32 @@ class ChatListBloc
renew: renew,
onError: (e) => capturedError = e,
);
_lastRoomsJson = null;
add(DataGathered((s) => s.copyWith(rooms: rooms)));
_updateAppBadge(rooms);
if (capturedError != null) throw capturedError!;
}
Future<void> refresh({bool renew = true, bool silent = false}) async {
final SessionSingleFlight _refreshFlight = SessionSingleFlight();
/// Concurrent callers (tab switch, poll, push, resume) join the running
/// refresh instead of each fetching and re-emitting the full room list.
Future<void> refresh({bool renew = true, bool silent = false}) =>
_refreshFlight.run(() => _refresh(renew: renew, silent: silent));
/// Skips the refresh when the list was fetched within [maxAge].
Future<void> refreshIfOlderThan(Duration maxAge) {
final lastFetch = state.lastFetch;
if (lastFetch != null &&
DateTime.now().millisecondsSinceEpoch - lastFetch <
maxAge.inMilliseconds) {
return Future.value();
}
return refresh();
}
Future<void> _refresh({required bool renew, required bool silent}) async {
if (!silent) add(RefetchStarted<ChatListState>());
Object? capturedError;
try {
@@ -88,6 +109,11 @@ class ChatListBloc
renew: renew,
onError: (e) => capturedError = e,
);
if (silent) {
if (_isUnchanged(rooms)) return;
} else {
_lastRoomsJson = null;
}
add(DataGathered((s) => s.copyWith(rooms: rooms)));
_updateAppBadge(rooms);
} catch (e) {
@@ -96,6 +122,23 @@ class ChatListBloc
if (capturedError != null) addLoadingError(capturedError!);
}
// Encoded room data of the last applied poll result; response headers are
// left out because they differ on every request.
String? _lastRoomsJson;
/// A background poll that returns the same rooms would otherwise rebuild the
/// list and re-persist the whole state every 15 s.
bool _isUnchanged(GetRoomResponse rooms) {
final encoded = jsonEncode(rooms.data);
final unchanged =
encoded == _lastRoomsJson &&
innerState?.rooms != null &&
state.error == null &&
!state.isLoading;
_lastRoomsJson = encoded;
return unchanged;
}
/// Creates (or resolves) a 1:1 chat and returns its room token, or null in
/// demo mode. Refreshes the list so the room shows up.
Future<String?> createDirectChat(String invite) async {
@@ -171,6 +214,7 @@ class ChatListBloc
}).toSet();
if (!changed) return;
final newRooms = GetRoomResponse(updated)..headers = rooms.headers;
_lastRoomsJson = null;
add(Emit((s) => s.copyWith(rooms: newRooms)));
_updateAppBadge(newRooms);
}