From 4312f11967954f3de5de2c2984a726b12a45b48f Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 24 Sep 2026 22:33:50 +0800 Subject: [PATCH] perf: isolate live bus map animation rebuilds --- lib/screens/bus_map_screen.dart | 29 ++++++++++++++--------- test/bus_map_screen_test.dart | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/screens/bus_map_screen.dart b/lib/screens/bus_map_screen.dart index 4c2653d..b8ab61d 100644 --- a/lib/screens/bus_map_screen.dart +++ b/lib/screens/bus_map_screen.dart @@ -854,14 +854,7 @@ class _BusMapScreenState extends State final map = _unsupported ? _buildUnsupportedNotice(theme) - : ValueListenableBuilder( - valueListenable: _animationTick, - builder: (context, _, _) => _buildMap( - theme, - drawSet, - controller.settings.mobileMapProvider, - ), - ); + : _buildMap(theme, drawSet, controller.settings.mobileMapProvider); final overlay = Stack( children: [ @@ -1237,14 +1230,19 @@ class _BusMapScreenState extends State MobileMapProvider mapProvider, ) { if (useGoogleMapsProviderFor(mapProvider)) { - return _buildGoogleMap(theme, drawSet); + // google_maps_flutter updates markers through widget configuration, so + // its map still needs the animation tick. The FlutterMap backend below + // can isolate ticks to one marker layer instead of rebuilding the map. + return ValueListenableBuilder( + valueListenable: _animationTick, + builder: (context, _, _) => _buildGoogleMap(theme, drawSet), + ); } return _buildFlutterMap(theme, drawSet); } Widget _buildFlutterMap(ThemeData theme, _BusMapDrawSet drawSet) { final buses = drawSet.buses; - final now = DateTime.now(); final geometry = _selectedGeometry; return FlutterMap( mapController: _mapController, @@ -1346,7 +1344,16 @@ class _BusMapScreenState extends State ), ], ), - MarkerLayer(markers: _osmBusMarkers(buses, now)), + // A selected route animates four times per second. Keep that hot + // rebuild below FlutterMap so tiles, route geometry, stop pins and + // clusters remain untouched between server snapshots. + ValueListenableBuilder( + valueListenable: _animationTick, + builder: (context, _, _) => MarkerLayer( + key: const ValueKey('bus-map-moving-marker-layer'), + markers: _osmBusMarkers(buses, DateTime.now()), + ), + ), ], ); } diff --git a/test/bus_map_screen_test.dart b/test/bus_map_screen_test.dart index d6e1ed3..281e386 100644 --- a/test/bus_map_screen_test.dart +++ b/test/bus_map_screen_test.dart @@ -648,6 +648,48 @@ void main() { expect(after.longitude, greaterThan(before.longitude)); }); + _mapTest('animation ticks rebuild only the moving marker layer', ( + tester, + log, + controller, + ) async { + await _pumpMap(tester, controller); + await _pumpUntil( + tester, + () => find.byType(BusMapBusMarker).evaluate().length == 2, + ); + await _selectTheResolvedBus(tester, log); + + final mapBefore = tester.widget(find.byType(FlutterMap)); + final tileLayerBefore = tester.widget(find.byType(TileLayer)); + final movingLayerBefore = tester.widget( + find.byKey(const ValueKey('bus-map-moving-marker-layer')), + ); + + await tester.pump(const Duration(milliseconds: 300)); + + expect( + identical(tester.widget(find.byType(FlutterMap)), mapBefore), + isTrue, + ); + expect( + identical( + tester.widget(find.byType(TileLayer)), + tileLayerBefore, + ), + isTrue, + ); + expect( + identical( + tester.widget( + find.byKey(const ValueKey('bus-map-moving-marker-layer')), + ), + movingLayerBefore, + ), + isFalse, + ); + }); + _mapTest('a bus the feed could not pin down is still named', ( tester, log,