Skip to content

Commit

Permalink
multi!: v2 prep & reflected changes from fleaflet/flutter_map#1742 (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
JaffaKetchup authored Dec 2, 2023
1 parent a315fdc commit 038c1b6
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 170 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## 1.0.0-preview
## [2.0.0] - 2023/12/XX

* Fixed "Exception: Buffer parameter must not be null" - [#3](https://github.com/fleaflet/flutter_map_cancellable_tile_provider/pull/3) for [core #1687](https://github.com/fleaflet/flutter_map/issues/1687)
* Multiple bug fixes and performance enhancements - [#4](https://github.com/fleaflet/flutter_map_cancellable_tile_provider/pull/4) reflecting [core #1742](https://github.com/fleaflet/flutter_map/pull/1742)
* Added `silenceExceptions` parameter - [#4](https://github.com/fleaflet/flutter_map_cancellable_tile_provider/pull/4) for [core #1703](https://github.com/fleaflet/flutter_map/issues/1703)

## [1.0.0] - 2023/10/09

* Initial version, awaiting flutter_map v6 release before v1
1 change: 0 additions & 1 deletion CODEOWNERS

This file was deleted.

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';
100 changes: 100 additions & 0 deletions lib/src/image_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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:meta/meta.dart';

@internal
@visibleForTesting
class CancellableNetworkImageProvider
extends ImageProvider<CancellableNetworkImageProvider> {
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 CancellableNetworkImageProvider({
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(
CancellableNetworkImageProvider key,
ImageDecoderCallback decode,
) =>
MultiFrameImageStreamCompleter(
codec: _load(key, decode),
scale: 1,
debugLabel: url,
informationCollector: () => [
DiagnosticsProperty('URL', url),
DiagnosticsProperty('Fallback URL', fallbackUrl),
DiagnosticsProperty('Current provider', key),
],
);

Future<Codec> _load(
CancellableNetworkImageProvider key,
ImageDecoderCallback decode, {
bool useFallback = false,
}) {
startedLoading();

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),
)
.whenComplete(finishedLoadingBytes)
.then((response) => ImmutableBuffer.fromUint8List(response.data!))
.then(decode)
.onError<Exception>((err, stack) {
scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key));
if (err is DioException && CancelToken.isCancel(err)) {
return ImmutableBuffer.fromUint8List(TileProvider.transparentImage)
.then(decode);
}
if (useFallback || fallbackUrl == null) {
if (!silenceExceptions) throw err;
return ImmutableBuffer.fromUint8List(TileProvider.transparentImage)
.then(decode);
}
return _load(key, decode, useFallback: true);
});
}

@override
SynchronousFuture<CancellableNetworkImageProvider> obtainKey(
ImageConfiguration configuration,
) =>
SynchronousFuture(this);

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CancellableNetworkImageProvider &&
fallbackUrl == null &&
url == other.url);

@override
int get hashCode =>
Object.hashAll([url, if (fallbackUrl != null) fallbackUrl]);
}
109 changes: 109 additions & 0 deletions lib/src/tile_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'dart:async';
import 'dart:collection';

import 'package:dio/dio.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_map/flutter_map.dart';

import '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 [CancellableNetworkImageProvider]
/// 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,
) =>
CancellableNetworkImageProvider(
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();
}
}
Loading

0 comments on commit 038c1b6

Please sign in to comment.