diff --git a/example/lib/main.dart b/example/lib/main.dart index c2ad69bd1..16fedacc4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; +import 'package:flutter_map_example/pages/cancellable_tile_provider/cancellable_tile_provider.dart'; import 'package:flutter_map_example/pages/circle.dart'; import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart'; import 'package:flutter_map_example/pages/epsg3413_crs.dart'; @@ -47,6 +48,8 @@ class MyApp extends StatelessWidget { ), home: const HomePage(), routes: { + CancellableTileProviderPage.route: (context) => + const CancellableTileProviderPage(), PolylinePage.route: (context) => const PolylinePage(), MapControllerPage.route: (context) => const MapControllerPage(), AnimatedMapControllerPage.route: (context) => diff --git a/example/lib/pages/cancellable_tile_provider/cancellable_tile_provider.dart b/example/lib/pages/cancellable_tile_provider/cancellable_tile_provider.dart new file mode 100644 index 000000000..d1618f24f --- /dev/null +++ b/example/lib/pages/cancellable_tile_provider/cancellable_tile_provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map_example/pages/cancellable_tile_provider/ctp_impl.dart'; +import 'package:flutter_map_example/widgets/drawer.dart'; +import 'package:latlong2/latlong.dart'; + +class CancellableTileProviderPage extends StatelessWidget { + static const String route = '/cancellable_tile_provider_page'; + + const CancellableTileProviderPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Cancellable Tile Provider')), + drawer: buildDrawer(context, CancellableTileProviderPage.route), + body: Column( + children: [ + const Padding( + padding: EdgeInsets.all(12), + child: Text( + '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!', + ), + ), + Expanded( + child: FlutterMap( + options: MapOptions( + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), + ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + tileProvider: CancellableNetworkTileProvider(), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/cancellable_tile_provider/ctp_impl.dart b/example/lib/pages/cancellable_tile_provider/ctp_impl.dart new file mode 100644 index 000000000..b291a9674 --- /dev/null +++ b/example/lib/pages/cancellable_tile_provider/ctp_impl.dart @@ -0,0 +1,115 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:http/http.dart'; +import 'package:http/retry.dart'; + +class CancellableNetworkTileProvider extends TileProvider { + CancellableNetworkTileProvider({ + super.headers, + BaseClient? httpClient, + }) : httpClient = httpClient ?? RetryClient(Client()); + + final BaseClient httpClient; + + @override + bool get supportsCancelLoading => true; + + @override + ImageProvider getImageWithCancelLoadingSupport( + TileCoordinates coordinates, + TileLayer options, + Future cancelLoading, + ) => + CancellableNetworkImageProvider( + url: getTileUrl(coordinates, options), + fallbackUrl: getTileFallbackUrl(coordinates, options), + headers: headers, + httpClient: httpClient, + cancelLoading: cancelLoading, + ); +} + +class CancellableNetworkImageProvider + extends ImageProvider { + final String url; + final String? fallbackUrl; + final BaseClient httpClient; + final Map headers; + final Future cancelLoading; + + const CancellableNetworkImageProvider({ + required this.url, + required this.fallbackUrl, + required this.headers, + required this.httpClient, + required this.cancelLoading, + }); + + @override + ImageStreamCompleter loadImage( + CancellableNetworkImageProvider key, + ImageDecoderCallback decode, + ) { + final chunkEvents = StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, chunkEvents, decode), + chunkEvents: chunkEvents.stream, + scale: 1, + debugLabel: url, + informationCollector: () => [ + DiagnosticsProperty('URL', url), + DiagnosticsProperty('Fallback URL', fallbackUrl), + DiagnosticsProperty('Current provider', key), + ], + ); + } + + @override + Future obtainKey( + ImageConfiguration configuration, + ) => + SynchronousFuture(this); + + Future _loadAsync( + CancellableNetworkImageProvider key, + StreamController chunkEvents, + ImageDecoderCallback decode, { + bool useFallback = false, + }) async { + final cancelToken = CancelToken(); + cancelLoading.then((_) => cancelToken.cancel()); + + final Uint8List bytes; + try { + final dio = Dio(); + final response = await dio.get( + useFallback ? fallbackUrl ?? '' : url, + cancelToken: cancelToken, + options: Options( + headers: headers, + responseType: ResponseType.bytes, + ), + ); + bytes = response.data!; + } on DioException catch (err) { + if (CancelToken.isCancel(err)) { + return decode( + await ImmutableBuffer.fromUint8List(TileProvider.transparentImage), + ); + } + if (useFallback || fallbackUrl == null) rethrow; + return _loadAsync(key, chunkEvents, decode, useFallback: true); + } catch (_) { + if (useFallback || fallbackUrl == null) rethrow; + return _loadAsync(key, chunkEvents, decode, useFallback: true); + } + + return decode(await ImmutableBuffer.fromUint8List(bytes)); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index 63527f99d..ac8562b81 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; +import 'package:flutter_map_example/pages/cancellable_tile_provider/cancellable_tile_provider.dart'; import 'package:flutter_map_example/pages/circle.dart'; import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart'; import 'package:flutter_map_example/pages/epsg3413_crs.dart'; @@ -153,6 +154,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { FallbackUrlNetworkPage.route, currentRoute, ), + _buildMenuItem( + context, + const Text('Cancellable Tile Provider'), + CancellableTileProviderPage.route, + currentRoute, + ), const Divider(), _buildMenuItem( context, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2e50967ea..dfba65897 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: url_launcher: ^6.1.10 shared_preferences: ^2.1.1 url_strategy: ^0.2.0 + http: ^1.1.0 + dio: ^5.3.2 dev_dependencies: flutter_lints: ^2.0.1