diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index a713c51d4..48077e8a7 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -49,24 +49,40 @@ 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}'; } @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 +90,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!, 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.