My app happened to load identical thumbhashes in a list, and I realized that the current version of thumbhash toImage()
never got loaded from the cache. They blink every time you scroll up and down. (Already set gaplessPlayback: true)
The ==
operator and hashCode
for Uint8List don't work as expected (see dart-lang/sdk#16335). The code provided below is a minimal example that you can use instead of toImage()
. In the Flutter framework, images are cached, but if you use MemoryImage
, it won't utilize the cache because it relies on the Uint8List hashCode, which means it will never be the same even if the content is identical.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:thumbhash/thumbhash.dart' as thumb;
import 'dart:ui' as ui;
Future<ui.Image> thumbHashDecodeImage({
required Uint8List thumbHash,
}) async {
final completer = Completer<ui.Image>();
final image = thumb.thumbHashToRGBA(thumbHash);
ui.decodeImageFromPixels(image.rgba, image.width, image.height,
ui.PixelFormat.rgba8888, completer.complete);
return completer.future;
}
class ThumbHashImage extends ImageProvider<ThumbHashImage> {
/// Creates an object that decodes a [thumbHash] as an image.
///
/// The arguments must not be null.
const ThumbHashImage(this.thumbHash, {this.scale = 1.0});
/// The bytes to decode into an image.
final Uint8List thumbHash;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
@override
Future<ThumbHashImage> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<ThumbHashImage>(this);
@override
ImageStreamCompleter load(ThumbHashImage key, DecoderCallback decode) =>
OneFrameImageStreamCompleter(_loadAsync(key));
Future<ImageInfo> _loadAsync(ThumbHashImage key) async {
assert(key == this);
final image = await thumbHashDecodeImage(
thumbHash: thumbHash,
);
return ImageInfo(image: image, scale: key.scale);
}
@override
bool operator ==(Object other) => other.runtimeType != runtimeType
? false
: other is ThumbHashImage &&
memEquals(thumbHash, other.thumbHash) &&
other.scale == scale;
@override
int get hashCode => thumbHash.reduce((value, element) => value + element.hashCode);
@override
String toString() => '$runtimeType($thumbHash, scale: $scale)';
}
bool memEquals(Uint8List bytes1, Uint8List bytes2) {
if (identical(bytes1, bytes2)) {
return true;
}
if (bytes1.lengthInBytes != bytes2.lengthInBytes) {
return false;
}
// Treat the original byte lists as lists of 8-byte words.
var numWords = bytes1.lengthInBytes ~/ 8;
var words1 = bytes1.buffer.asUint64List(0, numWords);
var words2 = bytes2.buffer.asUint64List(0, numWords);
for (var i = 0; i < words1.length; i += 1) {
if (words1[i] != words2[i]) {
return false;
}
}
// Compare any remaining bytes.
for (var i = words1.lengthInBytes; i < bytes1.lengthInBytes; i += 1) {
if (bytes1[i] != bytes2[i]) {
return false;
}
}
return true;
}
hashCode can be improved with a better hashCode implementation.