From 06c0bb0d1bab3e3abecef52736a607b372886923 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Sun, 18 Oct 2026 12:32:57 -0700 Subject: [PATCH] perf(mobile): skip unchanged message footer layout Signed-off-by: Tom Brow --- .../channels/message_footer_layout.dart | 14 +++-- .../channels/message_footer_layout_test.dart | 57 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/mobile/lib/features/channels/message_footer_layout.dart b/mobile/lib/features/channels/message_footer_layout.dart index a35409a9e2..6668b8a8f6 100644 --- a/mobile/lib/features/channels/message_footer_layout.dart +++ b/mobile/lib/features/channels/message_footer_layout.dart @@ -54,9 +54,7 @@ class _FooterLayout extends MultiChildRenderObjectWidget { BuildContext context, covariant RenderBox renderObject, ) { - (renderObject as _RenderMessageFooter) - ..overlap = overlap - ..markNeedsLayout(); + (renderObject as _RenderMessageFooter).overlap = overlap; } } @@ -70,9 +68,15 @@ class _RenderMessageFooter extends RenderBox RenderBox, ContainerBoxParentData > { - _RenderMessageFooter(this.overlap); + _RenderMessageFooter(this._overlap); - double overlap; + double _overlap; + double get overlap => _overlap; + set overlap(double value) { + if (_overlap == value) return; + _overlap = value; + markNeedsLayout(); + } @override void setupParentData(RenderBox child) { diff --git a/mobile/test/features/channels/message_footer_layout_test.dart b/mobile/test/features/channels/message_footer_layout_test.dart index 39b1cf3bf0..22f950dd82 100644 --- a/mobile/test/features/channels/message_footer_layout_test.dart +++ b/mobile/test/features/channels/message_footer_layout_test.dart @@ -1,8 +1,47 @@ import 'package:buzz/features/channels/message_footer_layout.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + testWidgets('unchanged footer geometry does not relayout its timeline row', ( + tester, + ) async { + var layouts = 0; + final overlap = ValueNotifier(8.0); + addTearDown(overlap.dispose); + await tester.pumpWidget( + MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: _LayoutCounter( + onLayout: () => layouts++, + child: ValueListenableBuilder( + valueListenable: overlap, + builder: (context, value, child) => MessageFooterLayout( + overlap: value, + footer: const SizedBox(width: 100, height: 32), + child: const SizedBox(width: 200, height: 60), + ), + ), + ), + ), + ), + ); + final initialLayouts = layouts; + tester.element(find.byType(MessageFooterLayout)).markNeedsBuild(); + await tester.pump(); + expect( + layouts, + initialLayouts, + reason: 'Rebuilding unchanged reactions must not dirty the row layout', + ); + overlap.value = 12; + await tester.pump(); + expect(layouts, greaterThan(initialLayouts)); + expect(tester.getSize(find.byType(MessageFooterLayout)).height, 80); + }); + testWidgets('footer global transforms and semantics include overlap offset', ( tester, ) async { @@ -118,3 +157,21 @@ void main() { }, ); } + +class _LayoutCounter extends SingleChildRenderObjectWidget { + const _LayoutCounter({required this.onLayout, required super.child}); + final VoidCallback onLayout; + @override + RenderObject createRenderObject(BuildContext context) => + _CountLayouts(onLayout); +} + +class _CountLayouts extends RenderProxyBox { + _CountLayouts(this.onLayout); + final VoidCallback onLayout; + @override + void performLayout() { + onLayout(); + super.performLayout(); + } +}