From 7b3f1cf294e0463e4027f5e21ffbbc249d8815c6 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Sun, 13 Jul 2025 12:39:03 +0300 Subject: [PATCH 1/3] fix: improve tile rendering by using stable keys for Tile widgets --- lib/src/layer/tile_layer/tile_layer.dart | 34 ++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 3dcba4c0f..d32cbd0fd 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -579,23 +579,29 @@ See: // tiles that are *ready* and at the target zoom level. // We're happy to do a bit of diligent work here, since tiles not rendered are // cycles saved later on in the render pipeline. - final tiles = _tileImageManager + final tileRenderers = _tileImageManager .getTilesToRender(visibleRange: visibleTileRange) - .map((tileRenderer) => Tile( - // Must be an ObjectKey, not a ValueKey using the coordinates, in - // case we remove and replace the TileImage with a different one. - key: ObjectKey(tileRenderer), - scaledTileDimension: _tileScaleCalculator.scaledTileDimension( - map.zoom, - tileRenderer.positionCoordinates.z, - ), - currentPixelOrigin: map.pixelOrigin, - tileImage: tileRenderer.tileImage, - positionCoordinates: tileRenderer.positionCoordinates, - tileBuilder: widget.tileBuilder, - )) .toList(); + // Create a map of TileRenderer to Tile widgets with a stable key to prevent rebuilding + final tiles = tileRenderers.map((tileRenderer) { + // Use the coordinates as a unique identifier for the tile + final tileKey = ValueKey( + '${tileRenderer.positionCoordinates}:${tileRenderer.tileImage.coordinates}'); + + return Tile( + key: tileKey, + scaledTileDimension: _tileScaleCalculator.scaledTileDimension( + map.zoom, + tileRenderer.positionCoordinates.z, + ), + currentPixelOrigin: map.pixelOrigin, + tileImage: tileRenderer.tileImage, + positionCoordinates: tileRenderer.positionCoordinates, + tileBuilder: widget.tileBuilder, + ); + }).toList(); + // Sort in render order. In reverse: // 1. Tiles at the current zoom. // 2. Tiles at the current zoom +/- 1. From 2dcf45d626b9a042d13b3b43a217904a9c99ef65 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Sun, 13 Jul 2025 12:39:15 +0300 Subject: [PATCH 2/3] feat: optimize tile rendering by managing state updates and separating TileImageWidget --- lib/src/layer/tile_layer/tile.dart | 69 +++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index a713c51d4..12bd3ff79 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -49,24 +49,41 @@ class Tile extends StatefulWidget { } class _TileState extends State { + late final String tileKey; + @override void initState() { super.initState(); - widget.tileImage.addListener(_onTileImageChange); + tileKey = '${widget.positionCoordinates}:${widget.tileImage.coordinates}'; + print('Tile initState: ${widget.tileImage.coordinates} with key: $tileKey'); } @override void dispose() { - widget.tileImage.removeListener(_onTileImageChange); super.dispose(); } - void _onTileImageChange() { - if (mounted) setState(() {}); + // Used to prevent unnecessary rebuilds + @override + void didUpdateWidget(Tile oldWidget) { + super.didUpdateWidget(oldWidget); + // We only care about position changes for rebuilds, not image content changes + // as those are handled by the TileImageWidget + if (oldWidget.currentPixelOrigin != widget.currentPixelOrigin || + oldWidget.scaledTileDimension != widget.scaledTileDimension) { + setState(() {}); + } } @override Widget build(BuildContext context) { + final tileImageWidget = RepaintBoundary( + child: TileImageWidget( + key: ValueKey(widget.tileImage.coordinates.toString()), + tileImage: widget.tileImage, + ), + ); + return Positioned( left: widget.positionCoordinates.x * widget.scaledTileDimension - widget.currentPixelOrigin.dx, @@ -74,12 +91,50 @@ class _TileState extends State { widget.currentPixelOrigin.dy, width: widget.scaledTileDimension, height: widget.scaledTileDimension, - child: widget.tileBuilder?.call(context, _tileImage, widget.tileImage) ?? - _tileImage, + child: widget.tileBuilder + ?.call(context, tileImageWidget, widget.tileImage) ?? + tileImageWidget, ); } +} + +/// A widget that displays a tile image. +/// +/// This widget is separated from the [Tile] class to prevent unnecessary rebuilds. +@immutable +class TileImageWidget extends StatefulWidget { + /// The tile image data. + final TileImage tileImage; + + /// Creates a new instance of [TileImageWidget]. + const TileImageWidget({ + super.key, + required this.tileImage, + }); + + @override + State createState() => _TileImageWidgetState(); +} + +class _TileImageWidgetState extends State { + @override + void initState() { + super.initState(); + widget.tileImage.addListener(_onTileImageChange); + } - Widget get _tileImage { + @override + void dispose() { + widget.tileImage.removeListener(_onTileImageChange); + super.dispose(); + } + + void _onTileImageChange() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { if (widget.tileImage.loadError && widget.tileImage.errorImage != null) { return Image( image: widget.tileImage.errorImage!, From 1aad2991adb77e7217884fe310b9819a3ad218b8 Mon Sep 17 00:00:00 2001 From: Reinis Sprogis Date: Sun, 13 Jul 2025 14:37:22 +0300 Subject: [PATCH 3/3] fix: remove debug print statement from Tile initState --- lib/src/layer/tile_layer/tile.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index 12bd3ff79..48077e8a7 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -55,7 +55,6 @@ class _TileState extends State { void initState() { super.initState(); tileKey = '${widget.positionCoordinates}:${widget.tileImage.coordinates}'; - print('Tile initState: ${widget.tileImage.coordinates} with key: $tileKey'); } @override