add in-memory tile cache, tile fade transitions, and performance HUD to map page
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
@@ -8,9 +9,14 @@ class HiveTileCache {
|
||||
|
||||
static const String _boxName = 'map_tile_cache_v1';
|
||||
static const int _maxEntries = 6000;
|
||||
static const int _maxMemoryEntries = 1024;
|
||||
static const Duration _staleAfter = Duration(days: 14);
|
||||
static const Duration _prefetchCooldown = Duration(seconds: 25);
|
||||
static final Map<String, Future<Uint8List?>> _inFlight =
|
||||
<String, Future<Uint8List?>>{};
|
||||
static final LinkedHashMap<String, Uint8List> _memoryCache =
|
||||
LinkedHashMap<String, Uint8List>();
|
||||
static final Map<String, int> _recentPrefetches = <String, int>{};
|
||||
|
||||
static Future<void> init() async {
|
||||
if (!Hive.isBoxOpen(_boxName)) {
|
||||
@@ -27,30 +33,68 @@ class HiveTileCache {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List?> getOrFetch(String url) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final box = _boxOrNull;
|
||||
if (box == null) return _fetchWithoutCache(url);
|
||||
final cached = _freshCachedBytes(box, url, now);
|
||||
if (cached != null) return cached;
|
||||
static Uint8List? peek(String url) {
|
||||
final inMemory = _memoryCache.remove(url);
|
||||
if (inMemory != null) {
|
||||
_memoryCache[url] = inMemory;
|
||||
return inMemory;
|
||||
}
|
||||
|
||||
return _fetchAndStore(box, url, now);
|
||||
final box = _boxOrNull;
|
||||
if (box == null) return null;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final cached = _freshCachedBytes(box, url, now);
|
||||
if (cached == null || cached.isEmpty) return null;
|
||||
_rememberInMemory(url, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
static void prefetchUrls(
|
||||
Iterable<String> urls, {
|
||||
int maxCount = 96,
|
||||
}) {
|
||||
static Future<Uint8List?> getOrFetch(String url) async {
|
||||
final immediate = peek(url);
|
||||
if (immediate != null) return immediate;
|
||||
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final box = _boxOrNull;
|
||||
if (box == null) {
|
||||
final bytes = await _fetchWithoutCache(url);
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
_rememberInMemory(url, bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
final bytes = await _fetchAndStore(box, url, now);
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
_rememberInMemory(url, bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static void prefetchUrls(Iterable<String> urls, {int maxCount = 48}) {
|
||||
final box = _boxOrNull;
|
||||
if (box == null) return;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
_recentPrefetches.removeWhere(
|
||||
(_, ts) => now - ts > _prefetchCooldown.inMilliseconds,
|
||||
);
|
||||
var queued = 0;
|
||||
for (final url in urls) {
|
||||
if (queued >= maxCount) break;
|
||||
if (_freshCachedBytes(box, url, now) != null) continue;
|
||||
if (_memoryCache.containsKey(url)) continue;
|
||||
if (box.containsKey(url)) continue;
|
||||
final lastPrefetchTs = _recentPrefetches[url];
|
||||
if (lastPrefetchTs != null &&
|
||||
now - lastPrefetchTs < _prefetchCooldown.inMilliseconds) {
|
||||
continue;
|
||||
}
|
||||
if (_inFlight.containsKey(url)) continue;
|
||||
_recentPrefetches[url] = now;
|
||||
queued += 1;
|
||||
_fetchAndStore(box, url, now);
|
||||
_fetchAndStore(box, url, now).then((bytes) {
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
_rememberInMemory(url, bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +133,7 @@ class HiveTileCache {
|
||||
if (response.statusCode != 200) return null;
|
||||
final bytes = response.bodyBytes;
|
||||
await box.put(url, <String, dynamic>{'ts': now, 'bytes': bytes});
|
||||
_rememberInMemory(url, bytes);
|
||||
await _pruneIfNeeded(box);
|
||||
return bytes;
|
||||
} catch (_) {
|
||||
@@ -116,13 +161,30 @@ class HiveTileCache {
|
||||
}
|
||||
|
||||
entries.sort((a, b) {
|
||||
final ats = (a.value is Map && a.value['ts'] is int) ? a.value['ts'] as int : 0;
|
||||
final bts = (b.value is Map && b.value['ts'] is int) ? b.value['ts'] as int : 0;
|
||||
final ats = (a.value is Map && a.value['ts'] is int)
|
||||
? a.value['ts'] as int
|
||||
: 0;
|
||||
final bts = (b.value is Map && b.value['ts'] is int)
|
||||
? b.value['ts'] as int
|
||||
: 0;
|
||||
return ats.compareTo(bts);
|
||||
});
|
||||
|
||||
for (var i = 0; i < overflow; i++) {
|
||||
await box.delete(entries[i].key);
|
||||
final key = entries[i].key;
|
||||
await box.delete(key);
|
||||
if (key is String) {
|
||||
_memoryCache.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _rememberInMemory(String url, Uint8List bytes) {
|
||||
if (bytes.isEmpty) return;
|
||||
_memoryCache.remove(url);
|
||||
_memoryCache[url] = bytes;
|
||||
while (_memoryCache.length > _maxMemoryEntries) {
|
||||
_memoryCache.remove(_memoryCache.keys.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:rra_app/pages/map/tiles/hive_tile_cache.dart';
|
||||
|
||||
class HiveTileImage extends StatefulWidget {
|
||||
const HiveTileImage({required this.url, super.key});
|
||||
const HiveTileImage({required this.url, this.onLoaded, super.key});
|
||||
|
||||
final String url;
|
||||
final ValueChanged<String>? onLoaded;
|
||||
|
||||
@override
|
||||
State<HiveTileImage> createState() => _HiveTileImageState();
|
||||
@@ -15,18 +16,30 @@ class HiveTileImage extends StatefulWidget {
|
||||
class _HiveTileImageState extends State<HiveTileImage> {
|
||||
Uint8List? _bytes;
|
||||
String? _loadingUrl;
|
||||
bool _reportedLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load(widget.url);
|
||||
_bytes = HiveTileCache.peek(widget.url);
|
||||
if (_bytes != null && _bytes!.isNotEmpty) {
|
||||
_reportLoaded();
|
||||
} else {
|
||||
_load(widget.url);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant HiveTileImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.url != widget.url) {
|
||||
_load(widget.url);
|
||||
_reportedLoaded = false;
|
||||
_bytes = HiveTileCache.peek(widget.url);
|
||||
if (_bytes != null && _bytes!.isNotEmpty) {
|
||||
_reportLoaded();
|
||||
} else {
|
||||
_load(widget.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,20 +51,29 @@ class _HiveTileImageState extends State<HiveTileImage> {
|
||||
setState(() {
|
||||
_bytes = bytes;
|
||||
});
|
||||
_reportLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
void _reportLoaded() {
|
||||
if (_reportedLoaded) return;
|
||||
_reportedLoaded = true;
|
||||
widget.onLoaded?.call(widget.url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bytes = _bytes;
|
||||
if (bytes == null || bytes.isEmpty) {
|
||||
return const ColoredBox(color: Color(0xFFE0E0E0));
|
||||
return const ColoredBox(color: Colors.transparent);
|
||||
}
|
||||
_reportLoaded();
|
||||
return Image.memory(
|
||||
bytes,
|
||||
scale: widget.url.contains('@2x') ? 2.0 : 1.0,
|
||||
fit: BoxFit.cover,
|
||||
gaplessPlayback: true,
|
||||
filterQuality: FilterQuality.low,
|
||||
filterQuality: FilterQuality.medium,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user