Skip to content

Commit

Permalink
Reflected changes from fleaflet/flutter_map#1742
Browse files Browse the repository at this point in the history
Improved internal structure
  • Loading branch information
JaffaKetchup committed Dec 1, 2023
1 parent a315fdc commit 02b61e3
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 166 deletions.
166 changes: 1 addition & 165 deletions lib/flutter_map_cancellable_tile_provider.dart
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';
88 changes: 88 additions & 0 deletions lib/src/image_provider.dart
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]);
}
111 changes: 111 additions & 0 deletions lib/src/tile_provider.dart
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();
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_map_cancellable_tile_provider
description: Plugin for flutter_map that provides a `TileProvider` with the capability to cancel unnecessary HTTP tile requests
version: 1.0.0
version: 2.0.0

homepage: https://github.com/fleaflet/flutter_map
repository: https://github.com/fleaflet/flutter_map_cancellable_tile_provider
Expand Down

0 comments on commit 02b61e3

Please sign in to comment.