Build a fresh tile provider per map

TileLayer disposes its provider; cache only the directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
This commit is contained in:
Krzysztof Rudnicki 2026-11-10 16:03:02 +01:00
parent b4e69dbff4
commit ca205fa67e
2 changed files with 23 additions and 11 deletions

View File

@ -37,25 +37,33 @@ TileProvider buildCachedTileProvider(String cacheDir, {Dio? dio}) =>
dio: dio,
);
Future<TileProvider>? _opened;
Future<String?>? _cacheDir;
/// The shared disk-cached OSM provider; opened once per process.
/// A disk-cached OSM provider; each map gets its own.
///
/// Only the cache directory is looked up once per process. A provider is
/// never shared: `TileLayer` disposes its provider when the map goes away
/// (a `NetworkTileProvider` closes its HTTP client then), so a shared one
/// would be dead for the second map.
///
/// If the cache directory cannot be had, the map still works over the
/// network without a cache, and the reason is logged at warning level.
Future<TileProvider> openOsmTileProvider() => _opened ??= _open();
/// network without a cache, and the reason is logged once at warning level.
Future<TileProvider> openOsmTileProvider() async {
final dir = await (_cacheDir ??= _resolveCacheDir());
return dir == null ? NetworkTileProvider() : buildCachedTileProvider(dir);
}
/// Forgets the opened provider (tests only).
/// Forgets the looked-up cache directory (tests only).
@visibleForTesting
void resetOsmTileProvider() => _opened = null;
void resetOsmTileProvider() => _cacheDir = null;
Future<TileProvider> _open() async {
Future<String?> _resolveCacheDir() async {
try {
final dir = await getApplicationCacheDirectory();
return buildCachedTileProvider('${dir.path}/osm_tiles');
return '${dir.path}/osm_tiles';
} on Object catch (error) {
logMapWarning('map tile cache unavailable, tiles will not persist: $error');
return NetworkTileProvider();
return null;
}
}

View File

@ -32,12 +32,14 @@ void main() {
expect(provider.supportsCancelLoading, isTrue);
});
test('openOsmTileProvider opens a cached provider once', () async {
test('every open is a fresh cached provider (TileLayer disposes it)', () async {
final dir = installFakePathProvider();
addTearDown(() => dir.deleteSync(recursive: true));
final first = await openOsmTileProvider();
final second = await openOsmTileProvider();
expect(first, isA<CachedTileProvider>());
expect(identical(await openOsmTileProvider(), first), isTrue);
expect(second, isA<CachedTileProvider>());
expect(identical(second, first), isFalse);
});
test('without a cache dir: network provider and a warning', () async {
@ -47,6 +49,8 @@ void main() {
provider = await openOsmTileProvider();
});
expect(provider, isA<NetworkTileProvider>());
// Looked up once: the second map falls back without a second warning.
expect(await openOsmTileProvider(), isA<NetworkTileProvider>());
expect(
lines.where((l) => l.contains('WARNING') && l.contains('persist')),
hasLength(1),