Skip to content

Commit 40d213f

Browse files
authored
Added cancellation support to TileProvider and surrounding mechanisms (#1622)
* Added cancellation support to `TileProvider` and surrounding mechanisms Cleanup `TileProvider` interface * Removed duplicate import * Improved documentation Close `NetworkTileProvider.httpClient` in `dispose` * Added example for cancellable `TileProvider`
1 parent e5a7ec7 commit 40d213f

13 files changed

+580
-119
lines changed

example/lib/main.dart

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_map_example/pages/animated_map_controller.dart';
3+
import 'package:flutter_map_example/pages/cancellable_tile_provider/cancellable_tile_provider.dart';
34
import 'package:flutter_map_example/pages/circle.dart';
45
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
56
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
@@ -47,6 +48,8 @@ class MyApp extends StatelessWidget {
4748
),
4849
home: const HomePage(),
4950
routes: <String, WidgetBuilder>{
51+
CancellableTileProviderPage.route: (context) =>
52+
const CancellableTileProviderPage(),
5053
PolylinePage.route: (context) => const PolylinePage(),
5154
MapControllerPage.route: (context) => const MapControllerPage(),
5255
AnimatedMapControllerPage.route: (context) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_map/flutter_map.dart';
3+
import 'package:flutter_map/plugin_api.dart';
4+
import 'package:flutter_map_example/pages/cancellable_tile_provider/ctp_impl.dart';
5+
import 'package:flutter_map_example/widgets/drawer.dart';
6+
import 'package:latlong2/latlong.dart';
7+
8+
class CancellableTileProviderPage extends StatelessWidget {
9+
static const String route = '/cancellable_tile_provider_page';
10+
11+
const CancellableTileProviderPage({Key? key}) : super(key: key);
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
return Scaffold(
16+
appBar: AppBar(title: const Text('Cancellable Tile Provider')),
17+
drawer: buildDrawer(context, CancellableTileProviderPage.route),
18+
body: Column(
19+
children: [
20+
const Padding(
21+
padding: EdgeInsets.all(12),
22+
child: Text(
23+
'This map uses a custom `TileProvider` that cancels HTTP requests for unnecessary tiles. This should help speed up tile loading and reduce unneccessary costly tile requests, mainly on the web!',
24+
),
25+
),
26+
Expanded(
27+
child: FlutterMap(
28+
options: MapOptions(
29+
initialCenter: const LatLng(51.5, -0.09),
30+
initialZoom: 5,
31+
cameraConstraint: CameraConstraint.contain(
32+
bounds: LatLngBounds(
33+
const LatLng(-90, -180),
34+
const LatLng(90, 180),
35+
),
36+
),
37+
),
38+
children: [
39+
TileLayer(
40+
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
41+
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
42+
tileProvider: CancellableNetworkTileProvider(),
43+
),
44+
],
45+
),
46+
),
47+
],
48+
),
49+
);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'dart:async';
2+
import 'dart:ui';
3+
4+
import 'package:dio/dio.dart';
5+
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/rendering.dart';
7+
import 'package:flutter_map/flutter_map.dart';
8+
import 'package:http/http.dart';
9+
import 'package:http/retry.dart';
10+
11+
class CancellableNetworkTileProvider extends TileProvider {
12+
CancellableNetworkTileProvider({
13+
super.headers,
14+
BaseClient? httpClient,
15+
}) : httpClient = httpClient ?? RetryClient(Client());
16+
17+
final BaseClient httpClient;
18+
19+
@override
20+
bool get supportsCancelLoading => true;
21+
22+
@override
23+
ImageProvider getImageWithCancelLoadingSupport(
24+
TileCoordinates coordinates,
25+
TileLayer options,
26+
Future<void> cancelLoading,
27+
) =>
28+
CancellableNetworkImageProvider(
29+
url: getTileUrl(coordinates, options),
30+
fallbackUrl: getTileFallbackUrl(coordinates, options),
31+
headers: headers,
32+
httpClient: httpClient,
33+
cancelLoading: cancelLoading,
34+
);
35+
}
36+
37+
class CancellableNetworkImageProvider
38+
extends ImageProvider<CancellableNetworkImageProvider> {
39+
final String url;
40+
final String? fallbackUrl;
41+
final BaseClient httpClient;
42+
final Map<String, String> headers;
43+
final Future<void> cancelLoading;
44+
45+
const CancellableNetworkImageProvider({
46+
required this.url,
47+
required this.fallbackUrl,
48+
required this.headers,
49+
required this.httpClient,
50+
required this.cancelLoading,
51+
});
52+
53+
@override
54+
ImageStreamCompleter loadImage(
55+
CancellableNetworkImageProvider key,
56+
ImageDecoderCallback decode,
57+
) {
58+
final chunkEvents = StreamController<ImageChunkEvent>();
59+
60+
return MultiFrameImageStreamCompleter(
61+
codec: _loadAsync(key, chunkEvents, decode),
62+
chunkEvents: chunkEvents.stream,
63+
scale: 1,
64+
debugLabel: url,
65+
informationCollector: () => [
66+
DiagnosticsProperty('URL', url),
67+
DiagnosticsProperty('Fallback URL', fallbackUrl),
68+
DiagnosticsProperty('Current provider', key),
69+
],
70+
);
71+
}
72+
73+
@override
74+
Future<CancellableNetworkImageProvider> obtainKey(
75+
ImageConfiguration configuration,
76+
) =>
77+
SynchronousFuture<CancellableNetworkImageProvider>(this);
78+
79+
Future<Codec> _loadAsync(
80+
CancellableNetworkImageProvider key,
81+
StreamController<ImageChunkEvent> chunkEvents,
82+
ImageDecoderCallback decode, {
83+
bool useFallback = false,
84+
}) async {
85+
final cancelToken = CancelToken();
86+
cancelLoading.then((_) => cancelToken.cancel());
87+
88+
final Uint8List bytes;
89+
try {
90+
final dio = Dio();
91+
final response = await dio.get<Uint8List>(
92+
useFallback ? fallbackUrl ?? '' : url,
93+
cancelToken: cancelToken,
94+
options: Options(
95+
headers: headers,
96+
responseType: ResponseType.bytes,
97+
),
98+
);
99+
bytes = response.data!;
100+
} on DioException catch (err) {
101+
if (CancelToken.isCancel(err)) {
102+
return decode(
103+
await ImmutableBuffer.fromUint8List(TileProvider.transparentImage),
104+
);
105+
}
106+
if (useFallback || fallbackUrl == null) rethrow;
107+
return _loadAsync(key, chunkEvents, decode, useFallback: true);
108+
} catch (_) {
109+
if (useFallback || fallbackUrl == null) rethrow;
110+
return _loadAsync(key, chunkEvents, decode, useFallback: true);
111+
}
112+
113+
return decode(await ImmutableBuffer.fromUint8List(bytes));
114+
}
115+
}

example/lib/widgets/drawer.dart

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22

33
import 'package:flutter_map_example/pages/animated_map_controller.dart';
4+
import 'package:flutter_map_example/pages/cancellable_tile_provider/cancellable_tile_provider.dart';
45
import 'package:flutter_map_example/pages/circle.dart';
56
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
67
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
@@ -153,6 +154,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) {
153154
FallbackUrlNetworkPage.route,
154155
currentRoute,
155156
),
157+
_buildMenuItem(
158+
context,
159+
const Text('Cancellable Tile Provider'),
160+
CancellableTileProviderPage.route,
161+
currentRoute,
162+
),
156163
const Divider(),
157164
_buildMenuItem(
158165
context,

example/pubspec.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ dependencies:
1717
url_launcher: ^6.1.10
1818
shared_preferences: ^2.1.1
1919
url_strategy: ^0.2.0
20+
http: ^1.1.0
21+
dio: ^5.3.2
2022

2123
dev_dependencies:
2224
flutter_lints: ^2.0.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
part of 'tile_layer.dart';
2+
3+
@Deprecated(
4+
'Prefer creating a custom `TileProvider` instead. '
5+
'This option has been deprecated as it is out of scope for the `TileLayer`. '
6+
'This option is deprecated since v6.',
7+
)
8+
typedef TemplateFunction = String Function(
9+
String str,
10+
Map<String, String> data,
11+
);
12+
13+
enum EvictErrorTileStrategy {
14+
/// Never evict images for tiles which failed to load.
15+
none,
16+
17+
/// Evict images for tiles which failed to load when they are pruned.
18+
dispose,
19+
20+
/// Evict images for tiles which failed to load and:
21+
/// - do not belong to the current zoom level AND/OR
22+
/// - are not visible, respecting the pruning buffer (the maximum of the
23+
/// [keepBuffer] and [panBuffer].
24+
notVisibleRespectMargin,
25+
26+
/// Evict images for tiles which failed to load and:
27+
/// - do not belong to the current zoom level AND/OR
28+
/// - are not visible
29+
notVisible,
30+
}
31+
32+
typedef ErrorTileCallBack = void Function(
33+
TileImage tile,
34+
Object error,
35+
StackTrace? stackTrace,
36+
);

lib/src/layer/tile_layer/tile_image.dart

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/widgets.dart';
24
import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
35
import 'package:flutter_map/src/layer/tile_layer/tile_display.dart';
@@ -35,6 +37,11 @@ class TileImage extends ChangeNotifier {
3537
/// An optional image to show when a loading error occurs.
3638
final ImageProvider? errorImage;
3739

40+
/// Completer that is completed when this object is disposed
41+
///
42+
/// Intended to allow [TileProvider]s to cancel unneccessary HTTP requests.
43+
final Completer<void> cancelLoading;
44+
3845
ImageProvider imageProvider;
3946

4047
/// True if an error occurred during loading.
@@ -58,6 +65,7 @@ class TileImage extends ChangeNotifier {
5865
required this.onLoadError,
5966
required TileDisplay tileDisplay,
6067
required this.errorImage,
68+
required this.cancelLoading,
6169
}) : _tileDisplay = tileDisplay,
6270
_animationController = tileDisplay.when(
6371
instantaneous: (_) => null,
@@ -126,6 +134,8 @@ class TileImage extends ChangeNotifier {
126134

127135
/// Initiate loading of the image.
128136
void load() {
137+
if (cancelLoading.isCompleted) return;
138+
129139
loadStarted = DateTime.now();
130140

131141
try {
@@ -230,6 +240,8 @@ class TileImage extends ChangeNotifier {
230240
}
231241
}
232242

243+
cancelLoading.complete();
244+
233245
_readyToDisplay = false;
234246
_animationController?.stop(canceled: false);
235247
_animationController?.value = 0.0;

lib/src/layer/tile_layer/tile_image_manager.dart

+10-4
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,16 @@ class TileImageManager {
125125
final tilesToReload = List<TileImage>.from(_tiles.values);
126126

127127
for (final tile in tilesToReload) {
128-
tile.imageProvider = layer.tileProvider.getImage(
129-
tileBounds.atZoom(tile.coordinates.z).wrap(tile.coordinates),
130-
layer,
131-
);
128+
tile.imageProvider = layer.tileProvider.supportsCancelLoading
129+
? layer.tileProvider.getImageWithCancelLoadingSupport(
130+
tileBounds.atZoom(tile.coordinates.z).wrap(tile.coordinates),
131+
layer,
132+
tile.cancelLoading.future,
133+
)
134+
: layer.tileProvider.getImage(
135+
tileBounds.atZoom(tile.coordinates.z).wrap(tile.coordinates),
136+
layer,
137+
);
132138
tile.load();
133139
}
134140
}

0 commit comments

Comments
 (0)