thumbnails: changed cancellation strategy
This commit is contained in:
parent
8dfcdfe052
commit
157fc60322
6 changed files with 97 additions and 82 deletions
|
@ -226,7 +226,6 @@ class ImageEntry {
|
|||
final addresses = await servicePolicy.call(
|
||||
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
|
||||
priority: ServiceCallPriority.background,
|
||||
debugLabel: 'findAddressesFromCoordinates-$path',
|
||||
);
|
||||
if (addresses != null && addresses.isNotEmpty) {
|
||||
final address = addresses.first;
|
||||
|
|
|
@ -13,6 +13,8 @@ class ImageFileService {
|
|||
static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
||||
static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
||||
|
||||
static const thumbnailPriority = ServiceCallPriority.asap;
|
||||
|
||||
static Future<void> getImageEntries() async {
|
||||
try {
|
||||
await platform.invokeMethod('getImageEntries');
|
||||
|
@ -55,7 +57,7 @@ class ImageFileService {
|
|||
return Future.sync(() => Uint8List(0));
|
||||
}
|
||||
|
||||
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) {
|
||||
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object taskKey}) {
|
||||
return servicePolicy.call(
|
||||
() async {
|
||||
if (width > 0 && height > 0) {
|
||||
|
@ -72,12 +74,15 @@ class ImageFileService {
|
|||
}
|
||||
return Uint8List(0);
|
||||
},
|
||||
priority: ServiceCallPriority.asap,
|
||||
debugLabel: 'getThumbnail-${entry.path}',
|
||||
cancellationKey: cancellationKey,
|
||||
priority: thumbnailPriority,
|
||||
key: taskKey,
|
||||
);
|
||||
}
|
||||
|
||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, thumbnailPriority);
|
||||
|
||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority);
|
||||
|
||||
static Stream<ImageOpEvent> delete(List<ImageEntry> entries) {
|
||||
try {
|
||||
return opChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
|
|
|
@ -51,7 +51,6 @@ class MetadataService {
|
|||
return null;
|
||||
},
|
||||
priority: ServiceCallPriority.background,
|
||||
debugLabel: 'getCatalogMetadata-${entry.path}',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,14 +6,12 @@ import 'package:flutter/foundation.dart';
|
|||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||
|
||||
class ServicePolicy {
|
||||
final Queue<_Task> _asapQueue, _normalQueue, _backgroundQueue;
|
||||
final Map<Object, _Task> _paused = {};
|
||||
final Queue<_Task> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue();
|
||||
List<Queue<_Task>> _queues;
|
||||
_Task _running;
|
||||
|
||||
ServicePolicy._private()
|
||||
: _asapQueue = Queue(),
|
||||
_normalQueue = Queue(),
|
||||
_backgroundQueue = Queue() {
|
||||
ServicePolicy._private() {
|
||||
_queues = [_asapQueue, _normalQueue, _backgroundQueue];
|
||||
}
|
||||
|
||||
|
@ -21,8 +19,39 @@ class ServicePolicy {
|
|||
Future<T> Function() platformCall, {
|
||||
ServiceCallPriority priority = ServiceCallPriority.normal,
|
||||
String debugLabel,
|
||||
Object cancellationKey,
|
||||
Object key,
|
||||
}) {
|
||||
var task = _paused.remove(key);
|
||||
if (task != null) {
|
||||
debugPrint('resume task with key=$key');
|
||||
}
|
||||
var completer = task?.completer ?? Completer<T>();
|
||||
task ??= _Task(
|
||||
() async {
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
|
||||
final result = await platformCall();
|
||||
completer.complete(result);
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
|
||||
_running = null;
|
||||
_pickNext();
|
||||
},
|
||||
completer,
|
||||
key,
|
||||
);
|
||||
_getQueue(priority).addLast(task);
|
||||
_pickNext();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<T> resume<T>(Object key, ServiceCallPriority priority) {
|
||||
var task = _paused.remove(key);
|
||||
if (task == null) return null;
|
||||
_getQueue(priority).addLast(task);
|
||||
_pickNext();
|
||||
return task.completer.future;
|
||||
}
|
||||
|
||||
Queue<_Task> _getQueue(ServiceCallPriority priority) {
|
||||
Queue<_Task> queue;
|
||||
switch (priority) {
|
||||
case ServiceCallPriority.asap:
|
||||
|
@ -36,23 +65,7 @@ class ServicePolicy {
|
|||
queue = _normalQueue;
|
||||
break;
|
||||
}
|
||||
final completer = Completer<T>();
|
||||
final wrapped = _Task(
|
||||
() async {
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
|
||||
final result = await platformCall();
|
||||
completer.complete(result);
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
|
||||
_running = null;
|
||||
_pickNext();
|
||||
},
|
||||
completer,
|
||||
cancellationKey,
|
||||
);
|
||||
queue.addLast(wrapped);
|
||||
|
||||
_pickNext();
|
||||
return completer.future;
|
||||
return queue;
|
||||
}
|
||||
|
||||
void _pickNext() {
|
||||
|
@ -62,25 +75,41 @@ class ServicePolicy {
|
|||
_running?.callback?.call();
|
||||
}
|
||||
|
||||
bool cancel(Object cancellationKey) {
|
||||
bool cancel(Object key, ServiceCallPriority priority) {
|
||||
var cancelled = false;
|
||||
final tasks = _queues.expand((q) => q.where((task) => task.cancellationKey == cancellationKey)).toList();
|
||||
tasks.forEach((task) => _queues.forEach((q) {
|
||||
if (q.remove(task)) {
|
||||
cancelled = true;
|
||||
task.completer.completeError(CancelledException());
|
||||
}
|
||||
}));
|
||||
final queue = _getQueue(priority);
|
||||
final tasks = queue.where((task) => task.key == key).toList();
|
||||
tasks.forEach((task) {
|
||||
if (queue.remove(task)) {
|
||||
cancelled = true;
|
||||
task.completer.completeError(CancelledException());
|
||||
}
|
||||
});
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
bool pause(Object key, ServiceCallPriority priority) {
|
||||
var paused = false;
|
||||
final queue = _getQueue(priority);
|
||||
final tasks = queue.where((task) => task.key == key).toList();
|
||||
tasks.forEach((task) {
|
||||
if (queue.remove(task)) {
|
||||
paused = true;
|
||||
_paused.putIfAbsent(key, () => task);
|
||||
}
|
||||
});
|
||||
return paused;
|
||||
}
|
||||
|
||||
bool isPaused(Object key) => _paused.containsKey(key);
|
||||
}
|
||||
|
||||
class _Task {
|
||||
final VoidCallback callback;
|
||||
final Completer completer;
|
||||
final Object cancellationKey;
|
||||
final Object key;
|
||||
|
||||
const _Task(this.callback, this.completer, this.cancellationKey);
|
||||
const _Task(this.callback, this.completer, this.key);
|
||||
}
|
||||
|
||||
class CancelledException {}
|
||||
|
|
|
@ -40,20 +40,20 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
void didUpdateWidget(ThumbnailRasterImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.entry != entry) {
|
||||
_cancelProvider();
|
||||
_pauseProvider();
|
||||
_initProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelProvider();
|
||||
_pauseProvider();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
|
||||
|
||||
void _cancelProvider() => _imageProvider?.cancel();
|
||||
void _pauseProvider() => _imageProvider?.pause();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -3,10 +3,8 @@ import 'dart:ui' as ui show Codec;
|
|||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/service_policy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||
ThumbnailProvider({
|
||||
|
@ -15,30 +13,34 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
this.scale = 1.0,
|
||||
}) : assert(entry != null),
|
||||
assert(extent != null),
|
||||
assert(scale != null);
|
||||
assert(scale != null) {
|
||||
_cancellationKey = _buildKey(ImageConfiguration.empty);
|
||||
}
|
||||
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final double scale;
|
||||
|
||||
final Object _cancellationKey = Uuid();
|
||||
Object _cancellationKey;
|
||||
|
||||
@override
|
||||
Future<ThumbnailProviderKey> obtainKey(ImageConfiguration configuration) {
|
||||
// configuration can be empty (e.g. when obtaining key for eviction)
|
||||
// so we do not compute the target width/height here
|
||||
// and pass it to the key, to use it later for image loading
|
||||
return SynchronousFuture<ThumbnailProviderKey>(ThumbnailProviderKey(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
devicePixelRatio: configuration.devicePixelRatio,
|
||||
scale: scale,
|
||||
));
|
||||
return SynchronousFuture<ThumbnailProviderKey>(_buildKey(configuration));
|
||||
}
|
||||
|
||||
ThumbnailProviderKey _buildKey(ImageConfiguration configuration) => ThumbnailProviderKey(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
devicePixelRatio: configuration.devicePixelRatio,
|
||||
scale: scale,
|
||||
);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
|
||||
return CancellableMultiFrameImageStreamCompleter(
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
|
@ -49,15 +51,17 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
|
||||
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
|
||||
final dimPixels = (extent * key.devicePixelRatio).round();
|
||||
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, cancellationKey: _cancellationKey);
|
||||
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, taskKey: _cancellationKey);
|
||||
return await decode(bytes ?? Uint8List(0));
|
||||
}
|
||||
|
||||
Future<void> cancel() async {
|
||||
if (servicePolicy.cancel(_cancellationKey)) {
|
||||
await evict();
|
||||
}
|
||||
@override
|
||||
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, handleError) {
|
||||
ImageFileService.resumeThumbnail(_cancellationKey);
|
||||
super.resolveStreamForKey(configuration, stream, key, handleError);
|
||||
}
|
||||
|
||||
void pause() => ImageFileService.cancelThumbnail(_cancellationKey);
|
||||
}
|
||||
|
||||
class ThumbnailProviderKey {
|
||||
|
@ -76,35 +80,14 @@ class ThumbnailProviderKey {
|
|||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is ThumbnailProviderKey && other.entry.uri == entry.uri && other.extent == extent && other.scale == scale;
|
||||
return other is ThumbnailProviderKey && other.entry.contentId == entry.contentId && other.extent == extent && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(entry.uri, extent, scale);
|
||||
int get hashCode => hashValues(entry.contentId, extent, scale);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ThumbnailProviderKey{uri=${entry.uri}, extent=$extent, scale=$scale}';
|
||||
}
|
||||
}
|
||||
|
||||
class CancellableMultiFrameImageStreamCompleter extends MultiFrameImageStreamCompleter {
|
||||
CancellableMultiFrameImageStreamCompleter({
|
||||
@required Future<ui.Codec> codec,
|
||||
@required double scale,
|
||||
Stream<ImageChunkEvent> chunkEvents,
|
||||
InformationCollector informationCollector,
|
||||
}) : super(
|
||||
codec: codec,
|
||||
scale: scale,
|
||||
chunkEvents: chunkEvents,
|
||||
informationCollector: informationCollector,
|
||||
);
|
||||
|
||||
@override
|
||||
void reportError({DiagnosticsNode context, dynamic exception, StackTrace stack, informationCollector, bool silent = false}) {
|
||||
// prevent default error reporting in case of planned cancellation
|
||||
if (exception is CancelledException) return;
|
||||
super.reportError(context: context, exception: exception, stack: stack, informationCollector: informationCollector, silent: silent);
|
||||
return 'ThumbnailProviderKey{contentId=${entry.contentId}, extent=$extent, scale=$scale}';
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue