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
+90 -32
View File
@@ -14,6 +14,7 @@ import '../../../state/app/infrastructure/loadable_state/view/loadable_state_con
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../utils/debouncer.dart';
import '../../../widget/chat_background.dart';
import '../../../widget/clickable_app_bar.dart';
import '../../../widget/user_avatar.dart';
@@ -118,6 +119,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
_markAsReadFinal();
_chatBlocRef?.leaveChat(widget.room.token);
_searchTextController.dispose();
Debouncer.cancel(_searchDebounceTag);
super.dispose();
}
@@ -150,6 +152,45 @@ class _ChatViewState extends State<ChatView> with RouteAware {
context.read<ChatBloc>().setToken(widget.room.token);
}
void _refetch({bool renew = false}) => _refresh();
// The built rows only depend on the chat data and the search state; the list
// itself rebuilds far more often (loading flips, keyboard, parent rebuilds),
// and re-sorting plus re-creating every bubble each time is wasted work.
Object? _itemsKey;
List<Widget> _items = const [];
List<Widget> _itemsFor(ChatState state) {
final key = (
state.chatResponse,
state.isLoadingOlder,
state.hasMoreOld,
_searchActive,
_searchQuery,
_activeMatchIndex,
widget.room,
);
if (key == _itemsKey) return _items;
_itemsKey = key;
final items = _buildMessages(state.chatResponse!).reversed.toList();
// reverse:true renders index 0 at the bottom, so the top marker
// (spinner / start-of-chat) goes at the end.
if (state.isLoadingOlder) {
items.add(const _LoadingOlderIndicator());
} else if (!state.hasMoreOld) {
items.add(
ChatBubble(
key: const ValueKey('chat-start'),
isSender: false,
bubbleData: GetChatResponseObject.getTextDummy('Anfang des Chats'),
chatData: widget.room,
refetch: _refetch,
),
);
}
return _items = items;
}
void _enterSearchMode() {
setState(() {
_searchActive = true;
@@ -163,6 +204,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
}
void _exitSearchMode() {
Debouncer.cancel(_searchDebounceTag);
setState(() {
_searchActive = false;
_searchQuery = '';
@@ -175,7 +217,22 @@ class _ChatViewState extends State<ChatView> with RouteAware {
});
}
late final String _searchDebounceTag =
'chat-search-${identityHashCode(this)}';
/// Matching re-scans the whole history and rebuilds every row, so it runs
/// once typing pauses instead of per keystroke.
void _onSearchChanged(String q) {
Debouncer.debounce(
_searchDebounceTag,
const Duration(milliseconds: 200),
() {
if (mounted) _applySearch(q);
},
);
}
void _applySearch(String q) {
final chatResponse = context.read<ChatBloc>().state.data?.chatResponse;
setState(() {
_searchQuery = q;
@@ -268,9 +325,13 @@ class _ChatViewState extends State<ChatView> with RouteAware {
messages.add(
ChatBubble(
isSender: false,
key: ValueKey(
'day-${elementDate.year}-${elementDate.month}-'
'${elementDate.day}',
),
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
refetch: _refetch,
),
);
}
@@ -286,6 +347,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
messages.add(
ChatBubble(
key: ValueKey(element.id),
isSender:
element.actorId == widget.selfId &&
(element.messageType ==
@@ -294,7 +356,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
GetRoomResponseObjectMessageType.deletedComment),
bubbleData: element,
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
refetch: _refetch,
isRead: element.id <= commonRead,
selfId: widget.selfId,
highlightQuery: highlightQuery,
@@ -317,17 +379,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
@override
Widget build(BuildContext context) {
// Swallow the first back gesture while the keyboard is visible so it
// dismisses the IME instead of popping the chat — matches platform UX
// expectations (e.g. WhatsApp/Telegram) and prevents accidental exits
// mid-typing.
final keyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
return PopScope(
canPop: !keyboardOpen,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
FocusManager.instance.primaryFocus?.unfocus();
},
return _KeyboardDismissPopScope(
child: Scaffold(
backgroundColor: const Color(0xffefeae2),
appBar: _searchActive
@@ -376,25 +428,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
state.currentToken == widget.room.token,
enablePullToRefresh: false,
child: (state, _) {
final items = _buildMessages(
state.chatResponse!,
).reversed.toList();
// reverse:true renders index 0 at the bottom, so the top
// marker (spinner / start-of-chat) goes at the end.
if (state.isLoadingOlder) {
items.add(const _LoadingOlderIndicator());
} else if (!state.hasMoreOld) {
items.add(
ChatBubble(
isSender: false,
bubbleData: GetChatResponseObject.getTextDummy(
'Anfang des Chats',
),
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
),
);
}
final items = _itemsFor(state);
_itemCount = items.length;
return ScrollablePositionedList.builder(
reverse: true,
@@ -448,3 +482,27 @@ class _LoadingOlderIndicator extends StatelessWidget {
),
);
}
/// Swallows the first back gesture while the keyboard is visible so it
/// dismisses the IME instead of popping the chat — matches platform UX
/// expectations (e.g. WhatsApp/Telegram) and prevents accidental exits
/// mid-typing. A separate widget so the per-frame inset changes during the
/// keyboard animation only rebuild this scope, not the whole chat.
class _KeyboardDismissPopScope extends StatelessWidget {
final Widget child;
const _KeyboardDismissPopScope({required this.child});
@override
Widget build(BuildContext context) {
final keyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
return PopScope(
canPop: !keyboardOpen,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
FocusManager.instance.primaryFocus?.unfocus();
},
child: child,
);
}
}