-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reflected changes from fleaflet/flutter_map#1742
Improved internal structure
- Loading branch information
1 parent
a315fdc
commit 02b61e3
Showing
4 changed files
with
201 additions
and
166 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,165 +1 @@ | ||
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'; | ||
|
||
/// [TileProvider] that fetches tiles from the network, with the capability to | ||
/// cancel unnecessary HTTP tile requests | ||
/// | ||
/// {@template tp-desc} | ||
/// | ||
/// Tiles that are removed/pruned before they are fully loaded do not need to | ||
/// complete (down)loading, and therefore do not need to complete the HTTP | ||
/// interaction. Cancelling these unnecessary tile requests early could: | ||
/// | ||
/// - Reduce tile loading durations (particularly on the web) | ||
/// - Reduce users' (cellular) data and cache space consumption | ||
/// - Reduce costly tile requests to tile servers* | ||
/// - Improve performance by reducing CPU and IO work | ||
/// | ||
/// This provider uses '[dio](https://pub.dev/packages/dio)', which supports | ||
/// aborting unnecessary HTTP requests in-flight, after they have already been | ||
/// sent. | ||
/// | ||
/// Although HTTP request abortion is supported on all platforms, it is | ||
/// especially useful on the web - and therefore recommended for web apps. This | ||
/// is because the web platform has a limited number of simulatous HTTP requests, | ||
/// and so closing the requests allows new requests to be made for new tiles. | ||
/// On other platforms, the other benefits may still occur, but may not be as | ||
/// visible as on the web. | ||
/// | ||
/// Once HTTP request abortion is [added to Dart's 'native' 'http' package (which already has a PR opened)](https://github.com/dart-lang/http/issues/424), `NetworkTileProvider` will be updated to take advantage of it, replacing and deprecating this provider. This tile provider is currently a seperate package and not the default due to the reliance on the additional Dio dependency. | ||
/// | ||
/// --- | ||
/// | ||
/// On the web, the 'User-Agent' header cannot be changed as specified in | ||
/// [TileLayer.tileProvider]'s documentation, due to a Dart/browser limitation. | ||
/// {@endtemplate} | ||
class CancellableNetworkTileProvider extends TileProvider { | ||
/// Create a [CancellableNetworkTileProvider] to fetch tiles from the network, | ||
/// with cancellation support | ||
/// | ||
/// {@macro tp-desc} | ||
CancellableNetworkTileProvider({super.headers}) : _dio = Dio(); | ||
|
||
final Dio _dio; | ||
|
||
@override | ||
bool get supportsCancelLoading => true; | ||
|
||
@override | ||
ImageProvider getImageWithCancelLoadingSupport( | ||
TileCoordinates coordinates, | ||
TileLayer options, | ||
Future<void> cancelLoading, | ||
) => | ||
_CNTPImageProvider( | ||
url: getTileUrl(coordinates, options), | ||
fallbackUrl: getTileFallbackUrl(coordinates, options), | ||
tileProvider: this, | ||
cancelLoading: cancelLoading, | ||
); | ||
|
||
@override | ||
void dispose() { | ||
_dio.close(); | ||
super.dispose(); | ||
} | ||
} | ||
|
||
class _CNTPImageProvider extends ImageProvider<_CNTPImageProvider> { | ||
final String url; | ||
final String? fallbackUrl; | ||
final CancellableNetworkTileProvider tileProvider; | ||
final Future<void> cancelLoading; | ||
|
||
const _CNTPImageProvider({ | ||
required this.url, | ||
required this.fallbackUrl, | ||
required this.tileProvider, | ||
required this.cancelLoading, | ||
}); | ||
|
||
@override | ||
ImageStreamCompleter loadImage( | ||
_CNTPImageProvider key, | ||
ImageDecoderCallback decode, | ||
) { | ||
final chunkEvents = StreamController<ImageChunkEvent>(); | ||
|
||
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<_CNTPImageProvider> obtainKey( | ||
ImageConfiguration configuration, | ||
) => | ||
SynchronousFuture<_CNTPImageProvider>(this); | ||
|
||
Future<Codec> _loadAsync( | ||
_CNTPImageProvider key, | ||
StreamController<ImageChunkEvent> chunkEvents, | ||
ImageDecoderCallback decode, { | ||
bool useFallback = false, | ||
}) async { | ||
final cancelToken = CancelToken(); | ||
unawaited(cancelLoading.then((_) => cancelToken.cancel())); | ||
|
||
try { | ||
final codec = decode( | ||
await ImmutableBuffer.fromUint8List( | ||
(await tileProvider._dio.get<Uint8List>( | ||
useFallback ? fallbackUrl! : url, | ||
cancelToken: cancelToken, | ||
options: Options( | ||
headers: tileProvider.headers, | ||
responseType: ResponseType.bytes, | ||
), | ||
)) | ||
.data!, | ||
), | ||
).catchError((e) { | ||
// ignore: only_throw_errors | ||
if (useFallback || fallbackUrl == null) throw e as Object; | ||
return _loadAsync(key, chunkEvents, decode, useFallback: true); | ||
}); | ||
|
||
cancelLoading.ignore(); | ||
return codec; | ||
} 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 (_) { | ||
// This redundancy necessary, do not remove | ||
if (useFallback || fallbackUrl == null) rethrow; | ||
return _loadAsync(key, chunkEvents, decode, useFallback: true); | ||
} | ||
} | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
(other is _CNTPImageProvider && fallbackUrl == null && url == other.url); | ||
|
||
@override | ||
int get hashCode => | ||
Object.hashAll([url, if (fallbackUrl != null) fallbackUrl]); | ||
} | ||
export 'src/tile_provider.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
part of 'tile_provider.dart'; | ||
|
||
class _CNTPImageProvider extends ImageProvider<_CNTPImageProvider> { | ||
final String url; | ||
final String? fallbackUrl; | ||
final Map<String, String> headers; | ||
final Dio dioClient; | ||
final Future<void> cancelLoading; | ||
final bool silenceExceptions; | ||
final void Function() startedLoading; | ||
final void Function() finishedLoadingBytes; | ||
|
||
const _CNTPImageProvider({ | ||
required this.url, | ||
required this.fallbackUrl, | ||
required this.headers, | ||
required this.dioClient, | ||
required this.cancelLoading, | ||
required this.silenceExceptions, | ||
required this.startedLoading, | ||
required this.finishedLoadingBytes, | ||
}); | ||
|
||
@override | ||
ImageStreamCompleter loadImage( | ||
_CNTPImageProvider key, | ||
ImageDecoderCallback decode, | ||
) { | ||
startedLoading(); | ||
|
||
return MultiFrameImageStreamCompleter( | ||
codec: _loadBytes(key, decode) | ||
.whenComplete(finishedLoadingBytes) | ||
.then(ImmutableBuffer.fromUint8List) | ||
.then(decode), | ||
scale: 1, | ||
debugLabel: url, | ||
informationCollector: () => [ | ||
DiagnosticsProperty('URL', url), | ||
DiagnosticsProperty('Fallback URL', fallbackUrl), | ||
DiagnosticsProperty('Current provider', key), | ||
], | ||
); | ||
} | ||
|
||
Future<Uint8List> _loadBytes( | ||
_CNTPImageProvider key, | ||
ImageDecoderCallback decode, { | ||
bool useFallback = false, | ||
}) { | ||
final cancelToken = CancelToken(); | ||
unawaited(cancelLoading.then((_) => cancelToken.cancel())); | ||
|
||
return dioClient | ||
.getUri<Uint8List>( | ||
Uri.parse(useFallback ? fallbackUrl ?? '' : url), | ||
cancelToken: cancelToken, | ||
options: Options(headers: headers, responseType: ResponseType.bytes), | ||
) | ||
.then((response) => response.data!) | ||
.catchError((Object err, StackTrace stack) { | ||
scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key)); | ||
if (err is DioException && CancelToken.isCancel(err)) { | ||
return TileProvider.transparentImage; | ||
} | ||
if (useFallback || fallbackUrl == null) { | ||
if (silenceExceptions) return TileProvider.transparentImage; | ||
return Future<Uint8List>.error(err, stack); | ||
} | ||
return _loadBytes(key, decode, useFallback: true); | ||
}); | ||
} | ||
|
||
@override | ||
SynchronousFuture<_CNTPImageProvider> obtainKey( | ||
ImageConfiguration configuration, | ||
) => | ||
SynchronousFuture(this); | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
(other is _CNTPImageProvider && fallbackUrl == null && url == other.url); | ||
|
||
@override | ||
int get hashCode => | ||
Object.hashAll([url, if (fallbackUrl != null) fallbackUrl]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import 'dart:async'; | ||
import 'dart:collection'; | ||
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'; | ||
|
||
part 'image_provider.dart'; | ||
|
||
/// [TileProvider] that fetches tiles from the network, with the capability to | ||
/// cancel unnecessary HTTP tile requests | ||
/// | ||
/// {@template fmctp-desc} | ||
/// | ||
/// Tiles that are removed/pruned before they are fully loaded do not need to | ||
/// complete (down)loading, and therefore do not need to complete the HTTP | ||
/// interaction. Cancelling these unnecessary tile requests early could: | ||
/// | ||
/// - Reduce tile loading durations (particularly on the web) | ||
/// - Reduce users' (cellular) data and cache space consumption | ||
/// - Reduce costly tile requests to tile servers* | ||
/// - Improve performance by reducing CPU and IO work | ||
/// | ||
/// This provider uses '[dio](https://pub.dev/packages/dio)', which supports | ||
/// aborting unnecessary HTTP requests in-flight, after they have already been | ||
/// sent. | ||
/// | ||
/// Although HTTP request abortion is supported on all platforms, it is | ||
/// especially useful on the web - and therefore recommended for web apps. This | ||
/// is because the web platform has a limited number of simulatous HTTP requests, | ||
/// and so closing the requests allows new requests to be made for new tiles. | ||
/// On other platforms, the other benefits may still occur, but may not be as | ||
/// visible as on the web. | ||
/// | ||
/// Once HTTP request abortion is | ||
/// [added to Dart's 'native' 'http' package (which already has a PR opened)](https://github.com/dart-lang/http/issues/424), | ||
/// `NetworkTileProvider` will be updated to take advantage of it, replacing and | ||
/// deprecating this provider. This tile provider is currently a seperate package | ||
/// and not the default due to the reliance on the additional Dio dependency. | ||
/// | ||
/// --- | ||
/// | ||
/// On the web, the 'User-Agent' header cannot be changed as specified in | ||
/// [TileLayer.tileProvider]'s documentation, due to a Dart/browser limitation. | ||
/// | ||
/// The [silenceExceptions] argument controls whether to ignore exceptions and | ||
/// errors that occur whilst fetching tiles over the network, and just return a | ||
/// transparent tile. | ||
/// {@endtemplate} | ||
base class CancellableNetworkTileProvider extends TileProvider { | ||
/// Create a [CancellableNetworkTileProvider] to fetch tiles from the network, | ||
/// with cancellation support | ||
/// | ||
/// {@macro fmctp-desc} | ||
CancellableNetworkTileProvider({ | ||
super.headers, | ||
Dio? dioClient, | ||
this.silenceExceptions = false, | ||
}) : _dioClient = dioClient ?? Dio(); | ||
|
||
/// Whether to ignore exceptions and errors that occur whilst fetching tiles | ||
/// over the network, and just return a transparent tile | ||
final bool silenceExceptions; | ||
|
||
/// Long living client used to make all tile requests by [_CNTPImageProvider] | ||
/// for the duration that this provider is alive | ||
final Dio _dioClient; | ||
|
||
/// Each [Completer] is completed once the corresponding tile has finished | ||
/// loading | ||
/// | ||
/// Used to avoid disposing of [_dioClient] whilst HTTP requests are still | ||
/// underway. | ||
/// | ||
/// Does not include tiles loaded from session cache. | ||
final _tilesInProgress = HashMap<TileCoordinates, Completer<void>>(); | ||
|
||
@override | ||
bool get supportsCancelLoading => true; | ||
|
||
@override | ||
ImageProvider getImageWithCancelLoadingSupport( | ||
TileCoordinates coordinates, | ||
TileLayer options, | ||
Future<void> cancelLoading, | ||
) => | ||
_CNTPImageProvider( | ||
url: getTileUrl(coordinates, options), | ||
fallbackUrl: getTileFallbackUrl(coordinates, options), | ||
headers: headers, | ||
dioClient: _dioClient, | ||
cancelLoading: cancelLoading, | ||
silenceExceptions: silenceExceptions, | ||
startedLoading: () => _tilesInProgress[coordinates] = Completer(), | ||
finishedLoadingBytes: () { | ||
_tilesInProgress[coordinates]?.complete(); | ||
_tilesInProgress.remove(coordinates); | ||
}, | ||
); | ||
|
||
@override | ||
Future<void> dispose() async { | ||
if (_tilesInProgress.isNotEmpty) { | ||
await Future.wait(_tilesInProgress.values.map((c) => c.future)); | ||
} | ||
_dioClient.close(); | ||
super.dispose(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters